From e97aa520c54a628db2fc45d4880e70064dc5abe2 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 30 Nov 2020 14:34:01 -0600 Subject: [PATCH 001/107] [visualizations] get index pattern via service instead of saved object (#84458) * embeddable - get index pattern via service instead of saved object --- .../public/embeddable/get_index_pattern.ts | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/plugins/visualizations/public/embeddable/get_index_pattern.ts b/src/plugins/visualizations/public/embeddable/get_index_pattern.ts index c12c95145fe44..22993eb6a2f6a 100644 --- a/src/plugins/visualizations/public/embeddable/get_index_pattern.ts +++ b/src/plugins/visualizations/public/embeddable/get_index_pattern.ts @@ -18,37 +18,19 @@ */ import { VisSavedObject } from '../types'; -import { - indexPatterns, - IIndexPattern, - IndexPatternAttributes, -} from '../../../../plugins/data/public'; -import { getUISettings, getSavedObjects } from '../services'; +import type { IndexPattern } from '../../../../plugins/data/public'; +import { getIndexPatterns } from '../services'; export async function getIndexPattern( savedVis: VisSavedObject -): Promise { +): Promise { if (savedVis.visState.type !== 'metrics') { return savedVis.searchSource!.getField('index'); } - const savedObjectsClient = getSavedObjects().client; - const defaultIndex = getUISettings().get('defaultIndex'); + const indexPatternsClient = getIndexPatterns(); - if (savedVis.visState.params.index_pattern) { - const indexPatternObjects = await savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title', 'fields'], - search: `"${savedVis.visState.params.index_pattern}"`, - searchFields: ['title'], - }); - const [indexPattern] = indexPatternObjects.savedObjects.map(indexPatterns.getFromSavedObject); - return indexPattern; - } - - const savedObject = await savedObjectsClient.get( - 'index-pattern', - defaultIndex - ); - return indexPatterns.getFromSavedObject(savedObject); + return savedVis.visState.params.index_pattern + ? (await indexPatternsClient.find(`"${savedVis.visState.params.index_pattern}"`))[0] + : await indexPatternsClient.getDefault(); } From 6e8895403a3af429777e806da5e959f1eda8e6aa Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 30 Nov 2020 22:19:32 +0000 Subject: [PATCH 002/107] chore(NA): tool to find plugins circular dependencies between plugins (#82867) * chore(NA): update gitignore to include first changes from moving into a single package.json * chore(NA): update gitignore * chore(NA): move all the dependencies into the single package.json and apply changes to bootstrap * chore(NA): fix types problems after the single package json * chore(NA): include code to find the dependencies used across the code * chore(NA): introduce pure lockfile for install dependencies on build * chore(NA): update clean task to not delete anything from xpack node_modules * chore(NA): update gitignore to remove development temporary rules * chore(NA): update notice file * chore(NA): update jest snapshots * chore(NA): fix whitelisted licenses to include a new specify form of an already included one * chore(NA): remove check lockfile symlinks from child projects * chore(NA): fix eslint and add missing declared deps on single pkg json * chore(NA): correctly update notice * chore(NA): fix failing jest test for storyshots.test.tsx * chore(NA): fix cypress multi reporter path * chore(NA): fix Project tests check * chore(NA): fix problem with logic to detect used dependes on oss build * chore(NA): include correct x-pack plugins dep discovery * chore(NA): discover entries under dynamic requires on vis_type_timelion * chore(NA): remove canvas * chore(NA): add initial code to find circular deps * chore(NA): ground work to integrate the circular deps scripts * chore(NA): add correct filtering to find circular dependenices feature * chore(NA): add ci mode flag into circular deps script * chore(NA): feature complete circular dependencies detect script * chore(NA): merge and solve conflicts with master * chore(NA): remove unwanted changes * chore(NA): remove unwanted changes on kbn storybook * chore(NA): hook find circular deps tool into ci * chore(NA): remove previous find plugin circular deps script * chore(NA): add type for circular dep list * chore(NA): add type for circular dep list for allowed list * chore(NA): allow CI to fail check * chore(NA): update deps allowed list * chore(NA): run search circular deps script over examples too * docs(NA): adds cli description * chore(NA): use plugin search paths to build entries to find circular deps * chore(NA): update allowed list * chore(NA): snapshot update for kbn optimizer test * chore(NA): update dpdm version * chore(NA): remove thirdParty flag * chore(NA): update docs to include info about the new tool * docs(NA): update to link PR instead of the issue * chore(NA): update debug logs to always output allowedList * fix(NA): correctly list found differences number * chore(NA): remove quiet flag * fix(NA): correctly fail the CI if circular deps are found * chore(NA): complete list of found circular deps * chore(NA): used named capturing group into the regex * docs(NA): update typescript best practices docs and styleguide * chore(NA): introduce quick filter option flag Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- STYLEGUIDE.md | 18 ++ .../best-practices/typescript.asciidoc | 4 +- package.json | 1 + ....js => find_plugins_with_circular_deps.js} | 2 +- src/dev/plugin_discovery/find_plugins.ts | 7 +- src/dev/run_find_plugin_circular_deps.ts | 73 ------ .../run_find_plugins_with_circular_deps.ts | 214 ++++++++++++++++++ .../checks/plugins_with_circular_deps.sh | 6 + vars/tasks.groovy | 1 + yarn.lock | 36 ++- 10 files changed, 283 insertions(+), 79 deletions(-) rename scripts/{find_plugin_circular_deps.js => find_plugins_with_circular_deps.js} (93%) delete mode 100644 src/dev/run_find_plugin_circular_deps.ts create mode 100644 src/dev/run_find_plugins_with_circular_deps.ts create mode 100644 test/scripts/checks/plugins_with_circular_deps.sh diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 94bb40ab3ff2e..cb75452a28cd2 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -589,6 +589,24 @@ Do not use setters, they cause more problems than they can solve. [sideeffect]: http://en.wikipedia.org/wiki/Side_effect_(computer_science) +### Avoid circular dependencies + +As part of a future effort to use correct and idempotent build tools we need our code to be +able to be represented as a directed acyclic graph. We must avoid having circular dependencies +both on code and type imports to achieve that. One of the most critical parts is the plugins +code. We've developed a tool to identify plugins with circular dependencies which +has allowed us to build a list of plugins who have circular dependencies +between each other. + +When building plugins we should avoid importing from plugins +who are known to have circular dependencies at the moment as well as introducing +new circular dependencies. You can run the same tool we use on our CI locally by +typing `node scripts/find_plugins_with_circular_deps --debug`. It will error out in +case new circular dependencies has been added with your changes +(which will also happen in the CI) as well as print out the current list of +the known circular dependencies which, as mentioned before, should not be imported +by your code until the circular dependencies on these have been solved. + ## SASS files When writing a new component, create a sibling SASS file of the same name and import directly into the **top** of the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index 6d298f92b841e..f6db3fdffcb6a 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -19,7 +19,7 @@ More details are available in the https://www.typescriptlang.org/docs/handbook/p ==== Caveats This architecture imposes several limitations to which we must comply: -- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. https://github.com/elastic/kibana/issues/78162 is going to provide a tool to find such problem places. +- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. We've built a tool that can be used to find such problems. Please read the prerequisites section below to know how to use it. - A project must emit its type declaration. It's not always possible to generate a type declaration if the compiler cannot infer a type. There are two basic cases: 1. Your plugin exports a type inferring an internal type declared in Kibana codebase. In this case, you'll have to either export an internal type or to declare an exported type explicitly. @@ -30,6 +30,8 @@ This architecture imposes several limitations to which we must comply: Since project refs rely on generated `d.ts` files, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. Run `node scripts/find_plugins_without_ts_refs.js --id your_plugin_id` to get a list of plugins that should be switched to TS project refs to unblock your plugin migration. +Additionally, in order to migrate into project refs, you also need to make sure your plugin doesn't have circular dependencies with other plugins both on code and type imports. We run a job in the CI for each PR trying to find if new circular dependencies are being added which runs our tool with `node scripts/find_plugins_with_circular_deps`. However there are also a couple of circular dependencies already identified and that are in an allowed list to be solved. You also need to make sure your plugin don't rely in any other plugin into that allowed list. For a complete overview of the circular dependencies both found and in the allowed list as well as the complete circular dependencies path please run the following script locally with the debug flag `node scripts/find_plugins_with_circular_deps --debug` . + [discrete] ==== Implementation - Make sure all the plugins listed as dependencies in *requiredPlugins*, *optionalPlugins* & *requiredBundles* properties of `kibana.json` manifest file have migrated to TS project references. diff --git a/package.json b/package.json index 7e23fcc7ae15c..7028b3093dc4f 100644 --- a/package.json +++ b/package.json @@ -619,6 +619,7 @@ "delete-empty": "^2.0.0", "dependency-check": "^4.1.0", "diff": "^4.0.1", + "dpdm": "3.5.0", "ejs": "^3.1.5", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", diff --git a/scripts/find_plugin_circular_deps.js b/scripts/find_plugins_with_circular_deps.js similarity index 93% rename from scripts/find_plugin_circular_deps.js rename to scripts/find_plugins_with_circular_deps.js index 6b0661cb841b4..138fec33fd6b4 100644 --- a/scripts/find_plugin_circular_deps.js +++ b/scripts/find_plugins_with_circular_deps.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/dev/run_find_plugin_circular_deps'); +require('../src/dev/run_find_plugins_with_circular_deps'); diff --git a/src/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts index 4e7c34698c964..e07c503e21330 100644 --- a/src/dev/plugin_discovery/find_plugins.ts +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -17,9 +17,12 @@ * under the License. */ import Path from 'path'; -import { REPO_ROOT } from '@kbn/dev-utils'; import { getPluginSearchPaths } from '@kbn/config'; -import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; +import { + KibanaPlatformPlugin, + REPO_ROOT, + simpleKibanaPlatformPluginDiscovery, +} from '@kbn/dev-utils'; export interface SearchOptions { oss: boolean; diff --git a/src/dev/run_find_plugin_circular_deps.ts b/src/dev/run_find_plugin_circular_deps.ts deleted file mode 100644 index 501e2c4fed048..0000000000000 --- a/src/dev/run_find_plugin_circular_deps.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { run } from '@kbn/dev-utils'; -import { findPlugins, getPluginDeps, SearchErrors } from './plugin_discovery'; - -interface AllOptions { - examples?: boolean; - extraPluginScanDirs?: string[]; -} - -run( - async ({ flags, log }) => { - const { examples = false, extraPluginScanDirs = [] } = flags as AllOptions; - - const pluginMap = findPlugins({ - oss: false, - examples, - extraPluginScanDirs, - }); - - const allErrors = new Map(); - for (const pluginId of pluginMap.keys()) { - const { errors } = getPluginDeps({ - pluginMap, - id: pluginId, - }); - - for (const [errorId, error] of errors) { - if (!allErrors.has(errorId)) { - allErrors.set(errorId, error); - } - } - } - - if (allErrors.size > 0) { - allErrors.forEach((error) => { - log.warning( - `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` - ); - }); - } - }, - { - flags: { - boolean: ['examples'], - default: { - examples: false, - }, - allowUnexpected: false, - help: ` - --examples Include examples folder - --extraPluginScanDirs Include extra scan folder - `, - }, - } -); diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts new file mode 100644 index 0000000000000..65a03a87525d7 --- /dev/null +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -0,0 +1,214 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import dedent from 'dedent'; +import { parseDependencyTree, parseCircular, prettyCircular } from 'dpdm'; +import { relative } from 'path'; +import { getPluginSearchPaths } from '@kbn/config'; +import { REPO_ROOT, run } from '@kbn/dev-utils'; + +interface Options { + debug?: boolean; + filter?: string; +} + +type CircularDepList = Set; + +const allowedList: CircularDepList = new Set([ + 'src/plugins/charts -> src/plugins/expressions', + 'src/plugins/charts -> src/plugins/vis_default_editor', + 'src/plugins/data -> src/plugins/embeddable', + 'src/plugins/data -> src/plugins/expressions', + 'src/plugins/data -> src/plugins/ui_actions', + 'src/plugins/embeddable -> src/plugins/ui_actions', + 'src/plugins/expressions -> src/plugins/visualizations', + 'src/plugins/vis_default_editor -> src/plugins/visualizations', + 'src/plugins/vis_default_editor -> src/plugins/visualize', + 'src/plugins/visualizations -> src/plugins/visualize', + 'x-pack/plugins/actions -> x-pack/plugins/case', + 'x-pack/plugins/apm -> x-pack/plugins/infra', + 'x-pack/plugins/lists -> x-pack/plugins/security_solution', + 'x-pack/plugins/security -> x-pack/plugins/spaces', +]); + +run( + async ({ flags, log }) => { + const { debug, filter } = flags as Options; + const foundList: CircularDepList = new Set(); + + const pluginSearchPathGlobs = getPluginSearchPaths({ + rootDir: REPO_ROOT, + oss: false, + examples: true, + }).map((pluginFolderPath) => `${relative(REPO_ROOT, pluginFolderPath)}/**/*`); + + const depTree = await parseDependencyTree(pluginSearchPathGlobs, { + context: REPO_ROOT, + }); + + // Build list of circular dependencies as well as the circular dependencies full paths + const circularDependenciesFullPaths = parseCircular(depTree).filter((circularDeps) => { + const first = circularDeps[0]; + const last = circularDeps[circularDeps.length - 1]; + const matchRegex = /(?(src|x-pack)\/plugins|examples|x-pack\/examples)\/(?[^\/]*)\/.*/; + const firstMatch = first.match(matchRegex); + const lastMatch = last.match(matchRegex); + + if ( + firstMatch?.groups?.pluginFolder && + firstMatch?.groups?.pluginName && + lastMatch?.groups?.pluginFolder && + lastMatch?.groups?.pluginName + ) { + const firstPlugin = `${firstMatch.groups.pluginFolder}/${firstMatch.groups.pluginName}`; + const lastPlugin = `${lastMatch.groups.pluginFolder}/${lastMatch.groups.pluginName}`; + const sortedPlugins = [firstPlugin, lastPlugin].sort(); + + // Exclude if both plugin paths involved in the circular dependency + // doesn't includes the provided filter + if (filter && !firstPlugin.includes(filter) && !lastPlugin.includes(filter)) { + return false; + } + + if (firstPlugin !== lastPlugin) { + foundList.add(`${sortedPlugins[0]} -> ${sortedPlugins[1]}`); + return true; + } + } + + return false; + }); + + if (!debug && filter) { + log.warning( + dedent(` + !!!!!!!!!!!!!! WARNING: FILTER WITHOUT DEBUG !!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Using the --filter flag without using --debug flag ! + ! will not allow you to see the filtered list of ! + ! the correct results. ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + `) + ); + } + + if (debug && filter) { + log.warning( + dedent(` + !!!!!!!!!!!!!!! WARNING: FILTER FLAG IS ON !!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Be aware the following results are not complete as ! + ! --filter flag has been passed. Ignore suggestions ! + ! to update the allowedList or any reports of failures ! + ! or successes. ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + The following filter has peen passed: ${filter} + `) + ); + } + + // Log the full circular dependencies path if we are under debug flag + if (debug && circularDependenciesFullPaths.length > 0) { + log.debug( + dedent(` + !!!!!!!!!!!!!! CIRCULAR DEPENDENCIES FOUND !!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! Circular dependencies were found, you can find below ! + ! all the paths involved. ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + `) + ); + log.debug(`${prettyCircular(circularDependenciesFullPaths)}\n`); + } + + // Always log the result of comparing the found list with the allowed list + const diffSet = (first: CircularDepList, second: CircularDepList) => + new Set([...first].filter((circularDep) => !second.has(circularDep))); + + const printList = (list: CircularDepList) => { + return Array.from(list) + .sort() + .reduce((listStr, entry) => { + return listStr ? `${listStr}\n'${entry}',` : `'${entry}',`; + }, ''); + }; + + const foundDifferences = diffSet(foundList, allowedList); + + if (debug && !foundDifferences.size) { + log.debug( + dedent(` + !!!!!!!!!!!!!!!!! UP TO DATE ALLOWED LIST !!!!!!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! The declared circular dependencies allowed list is up ! + ! to date and includes every plugin listed in above paths. ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + The allowed circular dependencies list is (#${allowedList.size}): + ${printList(allowedList)} + `) + ); + } + + if (foundDifferences.size > 0) { + log.error( + dedent(` + !!!!!!!!!!!!!!!!! OUT OF DATE ALLOWED LIST !!!!!!!!!!!!!!!!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ! The declared circular dependencies allowed list is out ! + ! of date. Please run the following locally to know more: ! + ! ! + ! 'node scripts/find_plugins_with_circular_deps --debug' ! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + The allowed circular dependencies list is (#${allowedList.size}): + ${printList(allowedList)} + + The found circular dependencies list is (#${foundList.size}): + ${printList(foundList)} + + The differences between both are (#${foundDifferences.size}): + ${printList(foundDifferences)} + + FAILED: circular dependencies in the allowed list declared on the file '${__filename}' did not match the found ones. + `) + ); + + process.exit(1); + } + + log.success('None non allowed circular dependencies were found'); + }, + { + description: + 'Searches circular dependencies between plugins located under src/plugins, x-pack/plugins, examples and x-pack/examples', + flags: { + boolean: ['debug'], + string: ['filter'], + default: { + debug: false, + }, + help: ` + --debug Run the script in debug mode which enables detailed path logs for circular dependencies + --filter It will only include in the results circular deps where the plugin paths contains parts of the passed string in the filter + `, + }, + } +); diff --git a/test/scripts/checks/plugins_with_circular_deps.sh b/test/scripts/checks/plugins_with_circular_deps.sh new file mode 100644 index 0000000000000..77880243538d2 --- /dev/null +++ b/test/scripts/checks/plugins_with_circular_deps.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +checks-reporter-with-killswitch "Check plugins with circular dependencies" \ + node scripts/find_plugins_with_circular_deps diff --git a/vars/tasks.groovy b/vars/tasks.groovy index b6bcc0d93f9c0..89302e91ad479 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -12,6 +12,7 @@ def check() { kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'), kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'), kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'), + kibanaPipeline.scriptTask('Check Plugins With Circular Dependencies', 'test/scripts/checks/plugins_with_circular_deps.sh'), kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'), kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'), kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'), diff --git a/yarn.lock b/yarn.lock index 2b89bd3417ed8..8644a53e6f52f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4626,6 +4626,13 @@ dependencies: "@types/jquery" "*" +"@types/fs-extra@^8.0.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" + integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== + dependencies: + "@types/node" "*" + "@types/geojson@*", "@types/geojson@7946.0.7": version "7946.0.7" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" @@ -11938,6 +11945,22 @@ dotignore@^0.1.2: dependencies: minimatch "^3.0.4" +dpdm@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/dpdm/-/dpdm-3.5.0.tgz#414402f21928694bc86cfe8e3583dc8fc97d013e" + integrity sha512-bff2gDpYyzmIOMwRp0Bsk0T4e/qgLRCeuGHZYEsJV0LRzuTUkXirCiLcme7Ebu/LVoQ8yAKiody5/1e51tsmFw== + dependencies: + "@types/fs-extra" "^8.0.0" + "@types/glob" "^7.1.1" + "@types/yargs" "^13.0.0" + chalk "^2.4.2" + fs-extra "^8.1.0" + glob "^7.1.4" + ora "^4.0.3" + tslib "^1.10.0" + typescript "^3.5.3" + yargs "^13.3.0" + dtrace-provider@~0.8: version "0.8.8" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" @@ -14125,6 +14148,15 @@ fs-extra@^7.0.0, fs-extra@^7.0.1, fs-extra@~7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" @@ -21330,7 +21362,7 @@ ora@^3.0.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -ora@^4.0.4: +ora@^4.0.3, ora@^4.0.4: version "4.1.1" resolved "https://registry.yarnpkg.com/ora/-/ora-4.1.1.tgz#566cc0348a15c36f5f0e979612842e02ba9dddbc" integrity sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A== @@ -27632,7 +27664,7 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@4.1.2, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.7.2: +typescript@4.1.2, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@^3.5.3, typescript@~3.7.2: version "4.1.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ== From d93e21133f01b810ab642c21bb419717e46d23ee Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 30 Nov 2020 15:54:52 -0700 Subject: [PATCH 003/107] Add application deep links to global search (#83380) --- ...ibana-plugin-core-public.app.exactroute.md | 2 +- .../public/kibana-plugin-core-public.app.md | 1 + ...-plugin-core-public.app.searchdeeplinks.md | 42 +++++++ ...na-plugin-core-public.appsearchdeeplink.md | 24 ++++ ...a-plugin-core-public.appupdatablefields.md | 2 +- .../core/public/kibana-plugin-core-public.md | 2 + ...kibana-plugin-core-public.publicappinfo.md | 3 +- ...core-public.publicappsearchdeeplinkinfo.md | 15 +++ src/core/public/application/index.ts | 2 + src/core/public/application/types.ts | 80 +++++++++++++- .../application/utils/get_app_info.test.ts | 36 ++++++ .../public/application/utils/get_app_info.ts | 32 +++++- .../chrome/nav_links/to_nav_link.test.ts | 1 + src/core/public/index.ts | 4 +- src/core/public/public.api.md | 24 +++- src/plugins/management/public/plugin.ts | 20 +++- .../public/components/search_bar.tsx | 2 +- .../public/providers/application.test.ts | 1 + .../public/providers/get_app_results.test.ts | 101 ++++++++++++++--- .../public/providers/get_app_results.ts | 91 ++++++++++++--- .../global_search/global_search_providers.ts | 104 ++++++++++-------- 21 files changed, 503 insertions(+), 86 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md diff --git a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md index d1e0be17a92b2..eb050b62c7d43 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md @@ -18,7 +18,7 @@ exactRoute?: boolean; ```ts core.application.register({ id: 'my_app', - title: 'My App' + title: 'My App', exactRoute: true, mount: () => { ... }, }) diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 7bdee9dc4c53e..8e8bae5ad9c58 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -27,6 +27,7 @@ export interface App | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | | [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md new file mode 100644 index 0000000000000..667fddbc212a5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) + +## App.searchDeepLinks property + +Array of links that represent secondary in-app locations for the app. + +Signature: + +```typescript +searchDeepLinks?: AppSearchDeepLink[]; +``` + +## Remarks + +Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. + +## Example + +The `path` property on deep links should not include the application's `appRoute`: + +```ts +core.application.register({ + id: 'my_app', + title: 'My App', + searchDeepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1' }, + { + id: 'sub2', + title: 'Sub2', + searchDeepLinks: [ + { id: 'subsub', title: 'SubSub', path: '/sub2/sub' } + ] + } + ], + mount: () => { ... }, +}) + +``` +Will produce deep links on these paths: - `/app/my_app/sub1` - `/app/my_app/sub2/sub` + diff --git a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md new file mode 100644 index 0000000000000..7e5ccf7d06ed1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) + +## AppSearchDeepLink type + +Input type for registering secondary in-app locations for an application. + +Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Signature: + +```typescript +export declare type AppSearchDeepLink = { + id: string; + title: string; +} & ({ + path: string; + searchDeepLinks?: AppSearchDeepLink[]; +} | { + path?: string; + searchDeepLinks: AppSearchDeepLink[]; +}); +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index 1232b7f940255..b6f404c3d11aa 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 6a90fd49f1d66..5f656b9ca510d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -138,6 +138,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. | | [AppMount](./kibana-plugin-core-public.appmount.md) | A mount function called when the user navigates to this app's route. | | [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | +| [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or searchDeepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppUnmount](./kibana-plugin-core-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | | [AppUpdater](./kibana-plugin-core-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | @@ -160,6 +161,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | +| [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index 3717dc847db25..d56b0ac58cd9b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,9 +9,10 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md new file mode 100644 index 0000000000000..9814f0408d047 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) + +## PublicAppSearchDeepLinkInfo type + +Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) + +Signature: + +```typescript +export declare type PublicAppSearchDeepLinkInfo = Omit & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +}; +``` diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 4f3b113a29c9b..b39aa70c888fe 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -31,6 +31,7 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, + AppSearchDeepLink, ApplicationSetup, ApplicationStart, AppLeaveHandler, @@ -40,6 +41,7 @@ export { AppLeaveConfirmAction, NavigateToAppOptions, PublicAppInfo, + PublicAppSearchDeepLinkInfo, // Internal types InternalApplicationSetup, InternalApplicationStart, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 02d2d3a52a01a..d9f326c7a59ab 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -81,7 +81,10 @@ export enum AppNavLinkStatus { * Defines the list of fields that can be updated via an {@link AppUpdater}. * @public */ -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick< + App, + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'searchDeepLinks' +>; /** * Updater for applications. @@ -222,7 +225,7 @@ export interface App { * ```ts * core.application.register({ * id: 'my_app', - * title: 'My App' + * title: 'My App', * exactRoute: true, * mount: () => { ... }, * }) @@ -232,18 +235,89 @@ export interface App { * ``` */ exactRoute?: boolean; + + /** + * Array of links that represent secondary in-app locations for the app. + * + * @remarks + * Used to populate navigational search results (where available). + * Can be updated using the {@link App.updater$} observable. See {@link AppSubLink} for more details. + * + * @example + * The `path` property on deep links should not include the application's `appRoute`: + * ```ts + * core.application.register({ + * id: 'my_app', + * title: 'My App', + * searchDeepLinks: [ + * { id: 'sub1', title: 'Sub1', path: '/sub1' }, + * { + * id: 'sub2', + * title: 'Sub2', + * searchDeepLinks: [ + * { id: 'subsub', title: 'SubSub', path: '/sub2/sub' } + * ] + * } + * ], + * mount: () => { ... }, + * }) + * ``` + * + * Will produce deep links on these paths: + * - `/app/my_app/sub1` + * - `/app/my_app/sub2/sub` + */ + searchDeepLinks?: AppSearchDeepLink[]; } +/** + * Input type for registering secondary in-app locations for an application. + * + * Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` + * represents a topological level in the application's hierarchy, but does not have a destination URL that is + * user-accessible. + * @public + */ +export type AppSearchDeepLink = { + /** Identifier to represent this sublink, should be unique for this application */ + id: string; + /** Title to label represent this deep link */ + title: string; +} & ( + | { + /** URL path to access this link, relative to the application's appRoute. */ + path: string; + /** Optional array of links that are 'underneath' this section in the hierarchy */ + searchDeepLinks?: AppSearchDeepLink[]; + } + | { + /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ + path?: string; + /** Array links that are 'underneath' this section in this hierarchy. */ + searchDeepLinks: AppSearchDeepLink[]; + } +); + +/** + * Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks} + * + * @public + */ +export type PublicAppSearchDeepLinkInfo = Omit & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +}; + /** * Public information about a registered {@link App | application} * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; /** diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index 055f7d1a5ada9..ee0bd4f1eadfa 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -43,6 +43,42 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, + searchDeepLinks: [], + }); + }); + + it('populates default values for nested searchDeepLinks', () => { + const app = createApp({ + searchDeepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + searchDeepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], + }, + ], + }); + const info = getAppInfo(app); + + expect(info).toEqual({ + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.visible, + appRoute: `/app/some-id`, + searchDeepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + searchDeepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + searchDeepLinks: [], // default empty array added + }, + ], + }, + ], }); }); diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index 71cd8a3e14929..7316080816da7 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -17,9 +17,16 @@ * under the License. */ -import { App, AppNavLinkStatus, AppStatus, PublicAppInfo } from '../types'; +import { + App, + AppNavLinkStatus, + AppStatus, + AppSearchDeepLink, + PublicAppInfo, + PublicAppSearchDeepLinkInfo, +} from '../types'; -export function getAppInfo(app: App): PublicAppInfo { +export function getAppInfo(app: App): PublicAppInfo { const navLinkStatus = app.navLinkStatus === AppNavLinkStatus.default ? app.status === AppStatus.inaccessible @@ -32,5 +39,26 @@ export function getAppInfo(app: App): PublicAppInfo { status: app.status!, navLinkStatus, appRoute: app.appRoute!, + searchDeepLinks: getSearchDeepLinkInfos(app, app.searchDeepLinks), }; } + +function getSearchDeepLinkInfos( + app: App, + searchDeepLinks?: AppSearchDeepLink[] +): PublicAppSearchDeepLinkInfo[] { + if (!searchDeepLinks) { + return []; + } + + return searchDeepLinks.map( + (rawDeepLink): PublicAppSearchDeepLinkInfo => { + return { + id: rawDeepLink.id, + title: rawDeepLink.title, + path: rawDeepLink.path, + searchDeepLinks: getSearchDeepLinkInfos(app, rawDeepLink.searchDeepLinks), + }; + } + ); +} diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 7e2c1fc1f89f8..606370c5afd0a 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -28,6 +28,7 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, + searchDeepLinks: [], ...props, }); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 564bbd712c535..557529fc94dc4 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -95,7 +95,6 @@ export { ApplicationSetup, ApplicationStart, App, - PublicAppInfo, AppMount, AppMountDeprecated, AppUnmount, @@ -110,6 +109,9 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, + AppSearchDeepLink, + PublicAppInfo, + PublicAppSearchDeepLinkInfo, ScopedHistory, NavigateToAppOptions, } from './application'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 37e57a9ee606e..aaea8f2f7c3fd 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -59,6 +59,8 @@ export interface App { mount: AppMount | AppMountDeprecated; navLinkStatus?: AppNavLinkStatus; order?: number; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppSubLink" + searchDeepLinks?: AppSearchDeepLink[]; status?: AppStatus; title: string; tooltip?: string; @@ -175,6 +177,18 @@ export enum AppNavLinkStatus { visible = 1 } +// @public +export type AppSearchDeepLink = { + id: string; + title: string; +} & ({ + path: string; + searchDeepLinks?: AppSearchDeepLink[]; +} | { + path?: string; + searchDeepLinks: AppSearchDeepLink[]; +}); + // @public export enum AppStatus { accessible = 0, @@ -185,7 +199,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -967,10 +981,16 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +}; + +// @public +export type PublicAppSearchDeepLinkInfo = Omit & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; // @public diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 122e73796753c..bf03c649fa6b4 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -31,6 +31,7 @@ import { AppUpdater, AppStatus, AppNavLinkStatus, + AppSearchDeepLink, } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; @@ -38,6 +39,7 @@ import { ManagementSectionsService, getSectionsServiceStartPrivate, } from './management_sections_service'; +import { ManagementSection } from './utils'; interface ManagementSetupDependencies { home?: HomePublicPluginSetup; @@ -46,7 +48,23 @@ interface ManagementSetupDependencies { export class ManagementPlugin implements Plugin { private readonly managementSections = new ManagementSectionsService(); - private readonly appUpdater = new BehaviorSubject(() => ({})); + private readonly appUpdater = new BehaviorSubject(() => { + const deepLinks: AppSearchDeepLink[] = Object.values( + this.managementSections.definedSections + ).map((section: ManagementSection) => ({ + id: section.id, + title: section.title, + searchDeepLinks: section.getAppsEnabled().map((mgmtApp) => ({ + id: mgmtApp.id, + title: mgmtApp.title, + path: mgmtApp.basePath, + })), + })); + + return { + searchDeepLinks: deepLinks, + }; + }); private hasAnyEnabledApps = true; diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index 3746e636066a9..ecd1c92bfcee6 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -83,7 +83,7 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi }; if (type === 'application') { - option.meta = [{ text: meta?.categoryLabel as string }]; + option.meta = [{ text: (meta?.categoryLabel as string) ?? '' }]; } else { option.meta = [{ text: cleanMeta(type) }]; } diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 2831550da00d9..7beed42de4c4f 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -28,6 +28,7 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, + searchDeepLinks: [], ...props, }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts index 5ef15a8cf2ea4..33fd358f61aca 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts @@ -10,7 +10,7 @@ import { PublicAppInfo, DEFAULT_APP_CATEGORIES, } from 'src/core/public'; -import { appToResult, getAppResults, scoreApp } from './get_app_results'; +import { AppLink, appToResult, getAppResults, scoreApp } from './get_app_results'; const createApp = (props: Partial = {}): PublicAppInfo => ({ id: 'app1', @@ -19,9 +19,17 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, + searchDeepLinks: [], ...props, }); +const createAppLink = (props: Partial = {}): AppLink => ({ + id: props.id ?? 'app1', + path: props.appRoute ?? '/app/app1', + subLinkTitles: [], + app: createApp(props), +}); + describe('getAppResults', () => { it('retrieves the matching results', () => { const apps = [ @@ -34,43 +42,82 @@ describe('getAppResults', () => { expect(results.length).toBe(1); expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 })); }); + + it('creates multiple links for apps with searchDeepLinks', () => { + const apps = [ + createApp({ + searchDeepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [] }, + { + id: 'sub2', + title: 'Sub2', + path: '/sub2', + searchDeepLinks: [ + { id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', searchDeepLinks: [] }, + ], + }, + ], + }), + ]; + + const results = getAppResults('App 1', apps); + + expect(results.length).toBe(4); + expect(results.map(({ title }) => title)).toEqual([ + 'App 1', + 'App 1 / Sub1', + 'App 1 / Sub2', + 'App 1 / Sub2 / Sub2Sub1', + ]); + }); + + it('only includes searchDeepLinks when search term is non-empty', () => { + const apps = [ + createApp({ + searchDeepLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [] }], + }), + ]; + + expect(getAppResults('', apps).length).toBe(1); + expect(getAppResults('App 1', apps).length).toBe(2); + }); }); describe('scoreApp', () => { describe('when the term is included in the title', () => { it('returns 100 if the app title is an exact match', () => { - expect(scoreApp('dashboard', createApp({ title: 'dashboard' }))).toBe(100); - expect(scoreApp('dashboard', createApp({ title: 'DASHBOARD' }))).toBe(100); - expect(scoreApp('DASHBOARD', createApp({ title: 'DASHBOARD' }))).toBe(100); - expect(scoreApp('dashBOARD', createApp({ title: 'DASHboard' }))).toBe(100); + expect(scoreApp('dashboard', createAppLink({ title: 'dashboard' }))).toBe(100); + expect(scoreApp('dashboard', createAppLink({ title: 'DASHBOARD' }))).toBe(100); + expect(scoreApp('DASHBOARD', createAppLink({ title: 'DASHBOARD' }))).toBe(100); + expect(scoreApp('dashBOARD', createAppLink({ title: 'DASHboard' }))).toBe(100); }); it('returns 90 if the app title starts with the term', () => { - expect(scoreApp('dash', createApp({ title: 'dashboard' }))).toBe(90); - expect(scoreApp('DASH', createApp({ title: 'dashboard' }))).toBe(90); + expect(scoreApp('dash', createAppLink({ title: 'dashboard' }))).toBe(90); + expect(scoreApp('DASH', createAppLink({ title: 'dashboard' }))).toBe(90); }); it('returns 75 if the term in included in the app title', () => { - expect(scoreApp('board', createApp({ title: 'dashboard' }))).toBe(75); - expect(scoreApp('shboa', createApp({ title: 'dashboard' }))).toBe(75); + expect(scoreApp('board', createAppLink({ title: 'dashboard' }))).toBe(75); + expect(scoreApp('shboa', createAppLink({ title: 'dashboard' }))).toBe(75); }); }); describe('when the term is not included in the title', () => { it('returns the levenshtein ratio if superior or equal to 60', () => { - expect(scoreApp('0123456789', createApp({ title: '012345' }))).toBe(60); - expect(scoreApp('--1234567-', createApp({ title: '123456789' }))).toBe(60); + expect(scoreApp('0123456789', createAppLink({ title: '012345' }))).toBe(60); + expect(scoreApp('--1234567-', createAppLink({ title: '123456789' }))).toBe(60); }); it('returns 0 if the levenshtein ratio is inferior to 60', () => { - expect(scoreApp('0123456789', createApp({ title: '12345' }))).toBe(0); - expect(scoreApp('1-2-3-4-5', createApp({ title: '123456789' }))).toBe(0); + expect(scoreApp('0123456789', createAppLink({ title: '12345' }))).toBe(0); + expect(scoreApp('1-2-3-4-5', createAppLink({ title: '123456789' }))).toBe(0); }); }); }); describe('appToResult', () => { it('converts an app to a result', () => { - const app = createApp({ + const app = createAppLink({ id: 'foo', title: 'Foo', euiIconType: 'fooIcon', @@ -92,7 +139,7 @@ describe('appToResult', () => { }); it('converts an app without category to a result', () => { - const app = createApp({ + const app = createAppLink({ id: 'foo', title: 'Foo', euiIconType: 'fooIcon', @@ -111,4 +158,28 @@ describe('appToResult', () => { score: 42, }); }); + + it('includes the app name in sub links', () => { + const app = createApp(); + const appLink: AppLink = { + id: 'app1-sub', + app, + path: '/sub1', + subLinkTitles: ['Sub1'], + }; + + expect(appToResult(appLink, 42).title).toEqual('App 1 / Sub1'); + }); + + it('does not include the app name in sub links for Stack Management', () => { + const app = createApp({ id: 'management' }); + const appLink: AppLink = { + id: 'management-sub', + app, + path: '/sub1', + subLinkTitles: ['Sub1'], + }; + + expect(appToResult(appLink, 42).title).toEqual('Sub1'); + }); }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts index c4e1a9532d144..01e6e87f30c94 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts @@ -5,22 +5,41 @@ */ import levenshtein from 'js-levenshtein'; -import { PublicAppInfo } from 'src/core/public'; +import { PublicAppInfo, PublicAppSearchDeepLinkInfo } from 'src/core/public'; import { GlobalSearchProviderResult } from '../../../global_search/public'; +/** Type used internally to represent an application unrolled into its separate searchDeepLinks */ +export interface AppLink { + id: string; + app: PublicAppInfo; + subLinkTitles: string[]; + path: string; +} + export const getAppResults = ( term: string, apps: PublicAppInfo[] ): GlobalSearchProviderResult[] => { - return apps - .map((app) => ({ app, score: scoreApp(term, app) })) - .filter(({ score }) => score > 0) - .map(({ app, score }) => appToResult(app, score)); + return ( + apps + // Unroll all searchDeepLinks, only if there is a search term + .flatMap((app) => + term.length > 0 + ? flattenDeepLinks(app) + : [{ id: app.id, app, path: app.appRoute, subLinkTitles: [] }] + ) + .map((appLink) => ({ + appLink, + score: scoreApp(term, appLink), + })) + .filter(({ score }) => score > 0) + .map(({ appLink, score }) => appToResult(appLink, score)) + ); }; -export const scoreApp = (term: string, { title }: PublicAppInfo): number => { +export const scoreApp = (term: string, appLink: AppLink): number => { term = term.toLowerCase(); - title = title.toLowerCase(); + const title = [appLink.app.title, ...appLink.subLinkTitles].join(' ').toLowerCase(); // shortcuts to avoid calculating the distance when there is an exact match somewhere. if (title === term) { @@ -43,17 +62,61 @@ export const scoreApp = (term: string, { title }: PublicAppInfo): number => { return 0; }; -export const appToResult = (app: PublicAppInfo, score: number): GlobalSearchProviderResult => { +export const appToResult = (appLink: AppLink, score: number): GlobalSearchProviderResult => { + const titleParts = + // Stack Management app should not include the app title in the concatenated link label + appLink.app.id === 'management' && appLink.subLinkTitles.length > 0 + ? appLink.subLinkTitles + : [appLink.app.title, ...appLink.subLinkTitles]; + return { - id: app.id, - title: app.title, + id: appLink.id, + // Concatenate title using slashes + title: titleParts.join(' / '), type: 'application', - icon: app.euiIconType, - url: app.appRoute, + icon: appLink.app.euiIconType, + url: appLink.path, meta: { - categoryId: app.category?.id ?? null, - categoryLabel: app.category?.label ?? null, + categoryId: appLink.app.category?.id ?? null, + categoryLabel: appLink.app.category?.label ?? null, }, score, }; }; + +const flattenDeepLinks = ( + app: PublicAppInfo, + deepLink?: PublicAppSearchDeepLinkInfo +): AppLink[] => { + if (!deepLink) { + return [ + { + id: app.id, + app, + path: app.appRoute, + subLinkTitles: [], + }, + ...app.searchDeepLinks.flatMap((appDeepLink) => flattenDeepLinks(app, appDeepLink)), + ]; + } + + return [ + ...(deepLink.path + ? [ + { + id: `${app.id}-${deepLink.id}`, + app, + subLinkTitles: [deepLink.title], + path: `${app.appRoute}${deepLink.path}`, + }, + ] + : []), + ...deepLink.searchDeepLinks + .flatMap((deepDeepLink) => flattenDeepLinks(app, deepDeepLink)) + .map((deepAppLink) => ({ + ...deepAppLink, + // shift current sublink title into array of sub-sublink titles + subLinkTitles: [deepLink.title, ...deepAppLink.subLinkTitles], + })), + ]; +}; diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index 16dc7b379214a..170548811def5 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -14,7 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); - const findResultsWithAPI = async (t: string): Promise => { + const findResultsWithApi = async (t: string): Promise => { return browser.executeAsync(async (term, cb) => { const { start } = window._coreProvider; const globalSearchTestApi: GlobalSearchTestApi = start.plugins.globalSearchTest; @@ -22,60 +22,76 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, t); }; - describe('GlobalSearch - SavedObject provider', function () { + describe('GlobalSearch providers', function () { before(async () => { - await esArchiver.load('global_search/basic'); + await pageObjects.common.navigateToApp('globalSearchTestApp'); }); - after(async () => { - await esArchiver.unload('global_search/basic'); - }); + describe('SavedObject provider', function () { + before(async () => { + await esArchiver.load('global_search/basic'); + }); - beforeEach(async () => { - await pageObjects.common.navigateToApp('globalSearchTestApp'); - }); + after(async () => { + await esArchiver.unload('global_search/basic'); + }); - it('can search for index patterns', async () => { - const results = await findResultsWithAPI('logstash'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('index-pattern'); - expect(results[0].title).to.be('logstash-*'); - expect(results[0].score).to.be.greaterThan(0.9); - }); + it('can search for index patterns', async () => { + const results = await findResultsWithApi('type:index-pattern logstash'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('index-pattern'); + expect(results[0].title).to.be('logstash-*'); + expect(results[0].score).to.be.greaterThan(0.9); + }); - it('can search for visualizations', async () => { - const results = await findResultsWithAPI('pie'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('visualization'); - expect(results[0].title).to.be('A Pie'); - }); + it('can search for visualizations', async () => { + const results = await findResultsWithApi('type:visualization pie'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('visualization'); + expect(results[0].title).to.be('A Pie'); + }); - it('can search for maps', async () => { - const results = await findResultsWithAPI('just'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('map'); - expect(results[0].title).to.be('just a map'); - }); + it('can search for maps', async () => { + const results = await findResultsWithApi('type:map just'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('map'); + expect(results[0].title).to.be('just a map'); + }); - it('can search for dashboards', async () => { - const results = await findResultsWithAPI('Amazing'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('dashboard'); - expect(results[0].title).to.be('Amazing Dashboard'); - }); + it('can search for dashboards', async () => { + const results = await findResultsWithApi('type:dashboard Amazing'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('dashboard'); + expect(results[0].title).to.be('Amazing Dashboard'); + }); - it('returns all objects matching the search', async () => { - const results = await findResultsWithAPI('dashboard'); - expect(results.length).to.be.greaterThan(2); - expect(results.map((r) => r.title)).to.contain('dashboard with map'); - expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); + it('returns all objects matching the search', async () => { + const results = await findResultsWithApi('type:dashboard dashboard'); + expect(results.length).to.be(2); + expect(results.map((r) => r.title)).to.contain('dashboard with map'); + expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); + }); + + it('can search by prefix', async () => { + const results = await findResultsWithApi('type:dashboard Amaz'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('dashboard'); + expect(results[0].title).to.be('Amazing Dashboard'); + }); }); - it('can search by prefix', async () => { - const results = await findResultsWithAPI('Amaz'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('dashboard'); - expect(results[0].title).to.be('Amazing Dashboard'); + describe('Applications provider', function () { + it('can search for root-level applications', async () => { + const results = await findResultsWithApi('discover'); + expect(results.length).to.be(1); + expect(results[0].title).to.be('Discover'); + }); + + it('can search for application deep links', async () => { + const results = await findResultsWithApi('saved objects'); + expect(results.length).to.be(1); + expect(results[0].title).to.be('Kibana / Saved Objects'); + }); }); }); } From 5ed39f2fe1585294bcdf6237c75284f129fe4a6b Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 30 Nov 2020 15:30:05 -0800 Subject: [PATCH 004/107] [Enterprise Search] Minor update to Elastic Cloud instructions copy per feedback (#84584) * Update Elastic Cloud instructions copy * this is what happens when you go OOO for a week * Update tests --- .../shared/setup_guide/cloud/instructions.test.tsx | 2 +- .../shared/setup_guide/cloud/instructions.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx index 3c93e3fd49dcc..56ed5f182fbb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx @@ -27,7 +27,7 @@ describe('CloudSetupInstructions', () => { ); const cloudDeploymentLink = wrapper.find(EuiLink).first(); expect(cloudDeploymentLink.prop('href')).toEqual( - 'https://cloud.elastic.co/deployments/some-id' + 'https://cloud.elastic.co/deployments/some-id/edit' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 7a7dfa62dbe39..383fd4b11108a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -23,18 +23,18 @@ export const CloudSetupInstructions: React.FC = ({ productName, cloudDepl steps={[ { title: i18n.translate('xpack.enterpriseSearch.setupGuide.cloud.step1.title', { - defaultMessage: 'Edit your Elastic Cloud deployment’s configuration', + defaultMessage: 'Edit your deployment’s configuration', }), children: (

- Visit the Elastic Cloud console + editDeploymentLink: cloudDeploymentLink ? ( + + edit your deployment ) : ( 'Visit the Elastic Cloud console' From 67564b9776abd608434c95375968598bfa082849 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 30 Nov 2020 19:23:26 -0800 Subject: [PATCH 005/107] Added default dedupKey value as an {{alertInstanceId}} to provide grouping functionality for PagerDuty incidents. (#84598) * Added default dedupKey value as an {{alertInstanceId}} to provide grouping functionality for PagerDuty incidents. * fixed type check --- x-pack/plugins/actions/README.md | 2 +- .../jira/jira_params.test.tsx | 3 ++- .../builtin_action_types/jira/jira_params.tsx | 5 ++-- .../pagerduty/pagerduty_params.test.tsx | 4 +++ .../resilient/resilient_params.test.tsx | 3 ++- .../resilient/resilient_params.tsx | 5 ++-- .../servicenow/servicenow_params.test.tsx | 3 ++- .../servicenow/servicenow_params.tsx | 5 ++-- .../text_field_with_message_variables.tsx | 3 +++ .../application/lib/action_variables.ts | 16 +++++++++--- .../get_defaults_for_action_params.test.ts | 25 +++++++++++++++++++ .../lib/get_defaults_for_action_params.ts | 25 +++++++++++++++++++ .../action_type_form.tsx | 7 ++++++ 13 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 432a4bfff7a6b..fb0293dca5ff4 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -529,7 +529,7 @@ The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com | Property | Description | Type | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | -| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. Defaults to `action:`. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | +| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | | summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | | source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | | severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx index 89a7c44c60dba..e7bec0b4b4452 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -9,6 +9,7 @@ import JiraParamsFields from './jira_params'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; import { ActionConnector } from '../../../../types'; +import { AlertProvidedActionVariables } from '../../../lib/action_variables'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_get_issue_types'); @@ -86,7 +87,7 @@ describe('JiraParamsFields renders', () => { errors={{ title: [] }} editAction={() => {}} index={0} - messageVariables={[{ name: 'alertId', description: '' }]} + messageVariables={[{ name: AlertProvidedActionVariables.alertId, description: '' }]} actionConnector={connector} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 385872ed67bc7..aaa9b697f32ec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -29,6 +29,7 @@ import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; import { SearchIssues } from './search_issues'; import { extractActionVariable } from '../extract_action_variable'; +import { AlertProvidedActionVariables } from '../../../lib/action_variables'; import { useKibana } from '../../../../common/lib/kibana'; const JiraParamsFields: React.FunctionComponent> = ({ @@ -51,7 +52,7 @@ const JiraParamsFields: React.FunctionComponent([]); const isActionBeingConfiguredByAnAlert = messageVariables - ? isSome(extractActionVariable(messageVariables, 'alertId')) + ? isSome(extractActionVariable(messageVariables, AlertProvidedActionVariables.alertId)) : false; useEffect(() => { @@ -144,7 +145,7 @@ const JiraParamsFields: React.FunctionComponent { expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( 'critical' ); + expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dedupKeyInput"]').first().prop('value')).toStrictEqual( + 'test' + ); expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx index cb9d96511abd5..5a57006cdf112 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx @@ -8,6 +8,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import ResilientParamsFields from './resilient_params'; import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; +import { AlertProvidedActionVariables } from '../../../lib/action_variables'; jest.mock('./use_get_incident_types'); jest.mock('./use_get_severity'); @@ -82,7 +83,7 @@ describe('ResilientParamsFields renders', () => { errors={{ title: [] }} editAction={() => {}} index={0} - messageVariables={[{ name: 'alertId', description: '' }]} + messageVariables={[{ name: AlertProvidedActionVariables.alertId, description: '' }]} actionConnector={connector} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx index 194dbe6712446..8c384903b86e4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -27,6 +27,7 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; import { extractActionVariable } from '../extract_action_variable'; +import { AlertProvidedActionVariables } from '../../../lib/action_variables'; import { useKibana } from '../../../../common/lib/kibana'; const ResilientParamsFields: React.FunctionComponent> = ({ @@ -46,7 +47,7 @@ const ResilientParamsFields: React.FunctionComponent { test('all params fields is rendered', () => { @@ -29,7 +30,7 @@ describe('ServiceNowParamsFields renders', () => { errors={{ title: [] }} editAction={() => {}} index={0} - messageVariables={[{ name: 'alertId', description: '' }]} + messageVariables={[{ name: AlertProvidedActionVariables.alertId, description: '' }]} /> ); expect(wrapper.find('[data-test-subj="urgencySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx index ee4e34cd1ab8b..240df24735414 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx @@ -22,6 +22,7 @@ import { ServiceNowActionParams } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { extractActionVariable } from '../extract_action_variable'; +import { AlertProvidedActionVariables } from '../../../lib/action_variables'; const ServiceNowParamsFields: React.FunctionComponent< ActionParamsProps @@ -30,7 +31,7 @@ const ServiceNowParamsFields: React.FunctionComponent< actionParams.subActionParams || {}; const isActionBeingConfiguredByAnAlert = messageVariables - ? isSome(extractActionVariable(messageVariables, 'alertId')) + ? isSome(extractActionVariable(messageVariables, AlertProvidedActionVariables.alertId)) : false; const selectOptions = [ @@ -73,7 +74,7 @@ const ServiceNowParamsFields: React.FunctionComponent< editAction('subAction', 'pushToService', index); } if (!savedObjectId && isActionBeingConfiguredByAnAlert) { - editSubActionProperty('savedObjectId', '{{alertId}}'); + editSubActionProperty('savedObjectId', `${AlertProvidedActionVariables.alertId}`); } if (!urgency) { editSubActionProperty('urgency', '3'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index 946bf064eb9ce..e2eba6b8a7f0f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -16,6 +16,7 @@ interface Props { inputTargetValue?: string; editAction: (property: string, value: any, index: number) => void; errors?: string[]; + defaultValue?: string | number | string[]; } export const TextFieldWithMessageVariables: React.FunctionComponent = ({ @@ -25,6 +26,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ inputTargetValue, editAction, errors, + defaultValue, }) => { const [currentTextElement, setCurrentTextElement] = useState(null); @@ -51,6 +53,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined} data-test-subj={`${paramsProperty}Input`} value={inputTargetValue || ''} + defaultValue={defaultValue} onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} onFocus={(e: React.FocusEvent) => { setCurrentTextElement(e.target); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index 296185211d043..d840f8ed3d1b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -19,6 +19,14 @@ export function transformActionVariables(actionVariables: ActionVariables): Acti return alwaysProvidedVars.concat(contextVars, paramsVars, stateVars); } +export enum AlertProvidedActionVariables { + alertId = 'alertId', + alertName = 'alertName', + spaceId = 'spaceId', + tags = 'tags', + alertInstanceId = 'alertInstanceId', +} + function prefixKeys(actionVariables: ActionVariable[], prefix: string): ActionVariable[] { return actionVariables.map((actionVariable) => { return { name: `${prefix}${actionVariable.name}`, description: actionVariable.description }; @@ -31,28 +39,28 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] { const result: ActionVariable[] = []; result.push({ - name: 'alertId', + name: AlertProvidedActionVariables.alertId, description: i18n.translate('xpack.triggersActionsUI.actionVariables.alertIdLabel', { defaultMessage: 'The id of the alert.', }), }); result.push({ - name: 'alertName', + name: AlertProvidedActionVariables.alertName, description: i18n.translate('xpack.triggersActionsUI.actionVariables.alertNameLabel', { defaultMessage: 'The name of the alert.', }), }); result.push({ - name: 'spaceId', + name: AlertProvidedActionVariables.spaceId, description: i18n.translate('xpack.triggersActionsUI.actionVariables.spaceIdLabel', { defaultMessage: 'The spaceId of the alert.', }), }); result.push({ - name: 'tags', + name: AlertProvidedActionVariables.tags, description: i18n.translate('xpack.triggersActionsUI.actionVariables.tagsLabel', { defaultMessage: 'The tags of the alert.', }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts new file mode 100644 index 0000000000000..c1f7cbb9fafed --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ResolvedActionGroup } from '../../../../alerts/common'; +import { AlertProvidedActionVariables } from './action_variables'; +import { getDefaultsForActionParams } from './get_defaults_for_action_params'; + +describe('getDefaultsForActionParams', () => { + test('pagerduty defaults', async () => { + expect(getDefaultsForActionParams('.pagerduty', 'test')).toEqual({ + dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, + eventAction: 'trigger', + }); + }); + + test('pagerduty defaults for resolved action group', async () => { + expect(getDefaultsForActionParams('.pagerduty', ResolvedActionGroup.id)).toEqual({ + dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, + eventAction: 'resolve', + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts new file mode 100644 index 0000000000000..c2143553e63c6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertActionParam, ResolvedActionGroup } from '../../../../alerts/common'; +import { AlertProvidedActionVariables } from './action_variables'; + +export const getDefaultsForActionParams = ( + actionTypeId: string, + actionGroupId: string +): Record | undefined => { + switch (actionTypeId) { + case '.pagerduty': + const pagerDutyDefaults = { + dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, + eventAction: 'trigger', + }; + if (actionGroupId === ResolvedActionGroup.id) { + pagerDutyDefaults.eventAction = 'resolve'; + } + return pagerDutyDefaults; + } +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index bd0e4b1645319..7e38805957931 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -42,6 +42,7 @@ import { ActionAccordionFormProps } from './action_form'; import { transformActionVariables } from '../../lib/action_variables'; import { resolvedActionGroupMessage } from '../../constants'; import { useKibana } from '../../../common/lib/kibana'; +import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; export type ActionTypeFormProps = { actionItem: AlertAction; @@ -108,6 +109,12 @@ export const ActionTypeForm = ({ ? resolvedActionGroupMessage : defaultActionMessage; setAvailableDefaultActionMessage(res); + const paramsDefaults = getDefaultsForActionParams(actionItem.actionTypeId, actionItem.group); + if (paramsDefaults) { + for (const [key, paramValue] of Object.entries(paramsDefaults)) { + setActionParamsProperty(key, paramValue, index); + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionItem.group]); From d4c02bb664617ab5c6f8fd3cae19ebcfc38ea961 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 1 Dec 2020 12:10:38 +0300 Subject: [PATCH 006/107] [Vega] Filter bar in Vega is not usable with non default index pattern. (#84090) * [Vega] Filtering is not working Closes: #81738 * fix CI * some work * some work * add tests for extract_index_pattern * simplify extract_index_pattern * fix type error * cleanup * Update vega_base_view.js * Update extract_index_pattern.test.ts * fix PR comments * fix some comments * findByTitle -> getByTitle * remove getByTitle * fix vega_base_view * fix jest * allow to set multiple indexes from top_nav Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data/common/index_patterns/utils.ts | 34 ++--- src/plugins/data/public/mocks.ts | 1 + .../public/metrics_type.ts | 14 +- .../public/data_model/es_query_parser.ts | 1 + .../vis_type_vega/public/data_model/types.ts | 5 +- .../public/data_model/vega_parser.test.js | 6 +- .../public/lib/extract_index_pattern.test.ts | 125 ++++++++++++++++++ .../public/lib/extract_index_pattern.ts | 47 +++++++ src/plugins/vis_type_vega/public/plugin.ts | 2 - src/plugins/vis_type_vega/public/services.ts | 11 +- src/plugins/vis_type_vega/public/vega_type.ts | 21 ++- .../public/vega_view/vega_base_view.js | 50 ++++++- .../public/vega_visualization.test.js | 3 +- .../public/vega_visualization.ts | 37 +----- .../components/visualize_top_nav.tsx | 35 +++-- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 17 files changed, 290 insertions(+), 110 deletions(-) create mode 100644 src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts create mode 100644 src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts diff --git a/src/plugins/data/common/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts index d9e1cfa0d952a..b7e1f28d5d60f 100644 --- a/src/plugins/data/common/index_patterns/utils.ts +++ b/src/plugins/data/common/index_patterns/utils.ts @@ -17,8 +17,8 @@ * under the License. */ -import { find } from 'lodash'; -import { SavedObjectsClientCommon, SavedObject } from '..'; +import type { IndexPatternSavedObjectAttrs } from './index_patterns'; +import type { SavedObjectsClientCommon } from '../types'; /** * Returns an object matching a given title @@ -27,24 +27,16 @@ import { SavedObjectsClientCommon, SavedObject } from '..'; * @param title {string} * @returns {Promise} */ -export async function findByTitle( - client: SavedObjectsClientCommon, - title: string -): Promise | void> { - if (!title) { - return Promise.resolve(); - } - - const savedObjects = await client.find({ - type: 'index-pattern', - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'], - }); +export async function findByTitle(client: SavedObjectsClientCommon, title: string) { + if (title) { + const savedObjects = await client.find({ + type: 'index-pattern', + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); - return find( - savedObjects, - (obj: SavedObject) => obj.attributes.title.toLowerCase() === title.toLowerCase() - ); + return savedObjects.find((obj) => obj.attributes.title.toLowerCase() === title.toLowerCase()); + } } diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 1b83eb569b1a1..67c1ff7e09dd7 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -64,6 +64,7 @@ const createStartContract = (): Start => { SearchBar: jest.fn().mockReturnValue(null), }, indexPatterns: ({ + find: jest.fn((search) => [{ id: search, title: search }]), createField: jest.fn(() => {}), createFieldList: jest.fn(() => []), ensureDefaultIndexPattern: jest.fn(), diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 2b75f69620629..41dc26c8c130d 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -24,7 +24,6 @@ import { PANEL_TYPES } from '../common/panel_types'; import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public'; import { getDataStart } from './services'; -import { INDEXES_SEPARATOR } from '../common/constants'; export const metricsVisDefinition = { name: 'metrics', @@ -84,18 +83,7 @@ export const metricsVisDefinition = { inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { const { indexPatterns } = getDataStart(); - const indexes: string = params.index_pattern; - if (indexes) { - const cachedIndexes = await indexPatterns.getIdsWithTitle(); - const ids = indexes - .split(INDEXES_SEPARATOR) - .map((title) => cachedIndexes.find((i) => i.title === title)?.id) - .filter((id) => id); - - return Promise.all(ids.map((id) => indexPatterns.get(id!))); - } - - return []; + return params.index_pattern ? await indexPatterns.find(params.index_pattern) : []; }, }; diff --git a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts index 1aac8e25d5c73..79eb049fb6dcc 100644 --- a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts @@ -226,6 +226,7 @@ export class EsQueryParser { const requestObject = requests.find((item) => getRequestName(item, index) === data.name); if (requestObject) { + requestObject.dataObject.url = requestObject.url; requestObject.dataObject.values = data.rawResponse; } }); diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index acd35e1747624..3bfe218099577 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -82,8 +82,9 @@ interface Projection { name: string; } -interface RequestDataObject { +interface RequestDataObject { name?: string; + url?: TUrlData; values: SearchResponse; } @@ -186,7 +187,7 @@ export interface CacheBounds { max: number; } -interface Requests { +interface Requests> { url: TUrlData; name: string; dataObject: TRequestDataObject; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 9fb80c6a1b19d..eb666d65b8670 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -185,21 +185,21 @@ describe('VegaParser._resolveEsQueries', () => { 'es', check( { data: { name: 'requestId', url: { index: 'a' }, x: 1 } }, - { data: { name: 'requestId', values: [42], x: 1 } } + { data: { name: 'requestId', url: { index: 'a', body: {} }, values: [42], x: 1 } } ) ); test( 'es 2', check( { data: { name: 'requestId', url: { '%type%': 'elasticsearch', index: 'a' } } }, - { data: { name: 'requestId', values: [42] } } + { data: { name: 'requestId', url: { index: 'a', body: {} }, values: [42] } } ) ); test( 'es arr', check( { arr: [{ data: { name: 'requestId', url: { index: 'a' }, x: 1 } }] }, - { arr: [{ data: { name: 'requestId', values: [42], x: 1 } }] } + { arr: [{ data: { name: 'requestId', url: { index: 'a', body: {} }, values: [42], x: 1 } }] } ) ); test( diff --git a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts new file mode 100644 index 0000000000000..a13428d539ad9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts @@ -0,0 +1,125 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { dataPluginMock } from '../../../data/public/mocks'; +import { extractIndexPatternsFromSpec } from './extract_index_pattern'; +import { setData } from '../services'; + +import type { VegaSpec } from '../data_model/types'; + +const getMockedSpec = (mockedObj: any) => (mockedObj as unknown) as VegaSpec; + +describe('extractIndexPatternsFromSpec', () => { + const dataStart = dataPluginMock.createStartContract(); + + beforeAll(() => { + setData(dataStart); + }); + + test('should not throw errors if no index is specified', async () => { + const spec = getMockedSpec({ + data: {}, + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(`Array []`); + }); + + test('should extract single index pattern', async () => { + const spec = getMockedSpec({ + data: { + url: { + index: 'test', + }, + }, + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test", + "title": "test", + }, + ] + `); + }); + + test('should extract multiple index patterns', async () => { + const spec = getMockedSpec({ + data: [ + { + url: { + index: 'test1', + }, + }, + { + url: { + index: 'test2', + }, + }, + ], + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test1", + "title": "test1", + }, + Object { + "id": "test2", + "title": "test2", + }, + ] + `); + }); + + test('should filter empty values', async () => { + const spec = getMockedSpec({ + data: [ + { + url: { + wrong: 'wrong', + }, + }, + { + url: { + index: 'ok', + }, + }, + ], + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(` + Array [ + Object { + "id": "ok", + "title": "ok", + }, + ] + `); + }); +}); diff --git a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts new file mode 100644 index 0000000000000..12cbd6f7ebbfa --- /dev/null +++ b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { flatten } from 'lodash'; +import { getData } from '../services'; + +import type { Data, VegaSpec } from '../data_model/types'; +import type { IndexPattern } from '../../../data/public'; + +export const extractIndexPatternsFromSpec = async (spec: VegaSpec) => { + const { indexPatterns } = getData(); + let data: Data[] = []; + + if (Array.isArray(spec.data)) { + data = spec.data; + } else if (spec.data) { + data = [spec.data]; + } + + return flatten( + await Promise.all( + data.reduce>>((accumulator, currentValue) => { + if (currentValue.url?.index) { + accumulator.push(indexPatterns.find(currentValue.url.index)); + } + + return accumulator; + }, []) + ) + ); +}; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 04481685c841b..55a69ab11966c 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -25,7 +25,6 @@ import { Setup as InspectorSetup } from '../../inspector/public'; import { setNotifications, setData, - setSavedObjects, setInjectedVars, setUISettings, setMapsLegacyConfig, @@ -100,7 +99,6 @@ export class VegaPlugin implements Plugin, void> { public start(core: CoreStart, { data }: VegaPluginStartDependencies) { setNotifications(core.notifications); - setSavedObjects(core.savedObjects); setData(data); setInjectedMetadata(core.injectedMetadata); } diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index 455fe67dbc423..43856c8324847 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -17,12 +17,7 @@ * under the License. */ -import { - CoreStart, - SavedObjectsStart, - NotificationsStart, - IUiSettingsClient, -} from 'src/core/public'; +import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/public'; import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../kibana_utils/public'; @@ -40,10 +35,6 @@ export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< CoreStart['injectedMetadata'] >('InjectedMetadata'); -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); - export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ enableExternalUrls: boolean; emsTileLayerId: unknown; diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 2211abb54aa93..d81bfe02389e2 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,19 +18,24 @@ */ import { i18n } from '@kbn/i18n'; -import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; +import { parse } from 'hjson'; +import type { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; + import { DefaultEditorSize } from '../../vis_default_editor/public'; -import { VegaVisualizationDependencies } from './plugin'; +import type { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; import { getDefaultSpec } from './default_spec'; +import { extractIndexPatternsFromSpec } from './lib/extract_index_pattern'; import { createInspectorAdapters } from './vega_inspector'; import { VIS_EVENT_TO_TRIGGER, VisGroups } from '../../visualizations/public'; import { toExpressionAst } from './to_ast'; -import { VisParams } from './vega_fn'; import { getInfoMessage } from './components/experimental_map_vis_info'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; +import type { VegaSpec } from './data_model/types'; +import type { VisParams } from './vega_fn'; + export const createVegaTypeDefinition = ( dependencies: VegaVisualizationDependencies ): BaseVisTypeOptions => { @@ -68,6 +73,16 @@ export const createVegaTypeDefinition = ( getSupportedTriggers: () => { return [VIS_EVENT_TO_TRIGGER.applyFilter]; }, + getUsedIndexPattern: async (visParams) => { + try { + const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); + + return extractIndexPatternsFromSpec(spec as VegaSpec); + } catch (e) { + // spec is invalid + } + return []; + }, inspectorAdapters: createInspectorAdapters, }; }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 25ea77ddbccb4..10f08edef1aa6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -27,7 +27,8 @@ import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; import { esFilters } from '../../../data/public'; -import { getEnableExternalUrls } from '../services'; +import { getEnableExternalUrls, getData } from '../services'; +import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; vega.scheme('elastic', euiPaletteColorBlind()); @@ -65,7 +66,6 @@ export class VegaBaseView { this._filterManager = opts.filterManager; this._fireEvent = opts.fireEvent; this._timefilter = opts.timefilter; - this._findIndex = opts.findIndex; this._view = null; this._vegaViewConfig = null; this._$messages = null; @@ -127,6 +127,48 @@ export class VegaBaseView { } } + /** + * Find index pattern by its title, if not given, gets it from spec or a defaults one + * @param {string} [index] + * @returns {Promise} index id + */ + async findIndex(index) { + const { indexPatterns } = getData(); + let idxObj; + + if (index) { + [idxObj] = await indexPatterns.find(index); + if (!idxObj) { + throw new Error( + i18n.translate('visTypeVega.vegaParser.baseView.indexNotFoundErrorMessage', { + defaultMessage: 'Index {index} not found', + values: { index: `"${index}"` }, + }) + ); + } + } else { + [idxObj] = await extractIndexPatternsFromSpec( + this._parser.isVegaLite ? this._parser.vlspec : this._parser.spec + ); + + if (!idxObj) { + const defaultIdx = await indexPatterns.getDefault(); + + if (defaultIdx) { + idxObj = defaultIdx; + } else { + throw new Error( + i18n.translate('visTypeVega.vegaParser.baseView.unableToFindDefaultIndexErrorMessage', { + defaultMessage: 'Unable to find default index', + }) + ); + } + } + } + + return idxObj.id; + } + createViewConfig() { const config = { // eslint-disable-next-line import/namespace @@ -261,7 +303,7 @@ export class VegaBaseView { * @param {string} [index] as defined in Kibana, or default if missing */ async addFilterHandler(query, index) { - const indexId = await this._findIndex(index); + const indexId = await this.findIndex(index); const filter = esFilters.buildQueryFilter(query, indexId); this._fireEvent({ name: 'applyFilter', data: { filters: [filter] } }); @@ -272,7 +314,7 @@ export class VegaBaseView { * @param {string} [index] as defined in Kibana, or default if missing */ async removeFilterHandler(query, index) { - const indexId = await this._findIndex(index); + const indexId = await this.findIndex(index); const filterToRemove = esFilters.buildQueryFilter(query, indexId); const currentFilters = this._filterManager.getFilters(); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a2214e139a296..8a073ca32b94a 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -30,7 +30,7 @@ import vegaMapGraph from './test_utils/vega_map_test.json'; import { VegaParser } from './data_model/vega_parser'; import { SearchAPI } from './data_model/search_api'; -import { setInjectedVars, setData, setSavedObjects, setNotifications } from './services'; +import { setInjectedVars, setData, setNotifications } from './services'; import { coreMock } from '../../../core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; @@ -76,7 +76,6 @@ describe('VegaVisualizations', () => { enableExternalUrls: true, }); setData(dataPluginStart); - setSavedObjects(coreStart.savedObjects); setNotifications(coreStart.notifications); vegaVisualizationDependencies = { diff --git a/src/plugins/vis_type_vega/public/vega_visualization.ts b/src/plugins/vis_type_vega/public/vega_visualization.ts index 58c436bcd4be4..554ac8962df46 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.ts +++ b/src/plugins/vis_type_vega/public/vega_visualization.ts @@ -20,13 +20,12 @@ import { i18n } from '@kbn/i18n'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { VegaParser } from './data_model/vega_parser'; import { VegaVisualizationDependencies } from './plugin'; -import { getNotifications, getData, getSavedObjects } from './services'; +import { getNotifications, getData } from './services'; import type { VegaView } from './vega_view/vega_view'; export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizationDependencies) => class VegaVisualization { private readonly dataPlugin = getData(); - private readonly savedObjectsClient = getSavedObjects(); private vegaView: InstanceType | null = null; constructor( @@ -34,39 +33,6 @@ export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizatio private fireEvent: IInterpreterRenderHandlers['event'] ) {} - /** - * Find index pattern by its title, of if not given, gets default - * @param {string} [index] - * @returns {Promise} index id - */ - async findIndex(index: string) { - const { indexPatterns } = this.dataPlugin; - let idxObj; - - if (index) { - // @ts-expect-error - idxObj = indexPatterns.findByTitle(this.savedObjectsClient, index); - if (!idxObj) { - throw new Error( - i18n.translate('visTypeVega.visualization.indexNotFoundErrorMessage', { - defaultMessage: 'Index {index} not found', - values: { index: `"${index}"` }, - }) - ); - } - } else { - idxObj = await indexPatterns.getDefault(); - if (!idxObj) { - throw new Error( - i18n.translate('visTypeVega.visualization.unableToFindDefaultIndexErrorMessage', { - defaultMessage: 'Unable to find default index', - }) - ); - } - } - return idxObj.id; - } - async render(visData: VegaParser) { const { toasts } = getNotifications(); @@ -112,7 +78,6 @@ export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizatio serviceSettings, filterManager, timefilter, - findIndex: this.findIndex.bind(this), }; if (vegaParser.useMap) { diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index b207529c456a1..4b32880136146 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -20,7 +20,6 @@ import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import { AppMountParameters, OverlayRef } from 'kibana/public'; -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../kibana_react/public'; import { @@ -31,6 +30,7 @@ import { } from '../types'; import { APP_NAME } from '../visualize_constants'; import { getTopNavConfig } from '../utils'; +import type { IndexPattern } from '../../../../data/public'; interface VisualizeTopNavProps { currentAppState: VisualizeAppState; @@ -118,7 +118,9 @@ const TopNav = ({ stateTransfer, onAppLeave, ]); - const [indexPattern, setIndexPattern] = useState(vis.data.indexPattern); + const [indexPatterns, setIndexPatterns] = useState( + vis.data.indexPattern ? [vis.data.indexPattern] : [] + ); const showDatePicker = () => { // tsvb loads without an indexPattern initially (TODO investigate). // hide timefilter only if timeFieldName is explicitly undefined. @@ -165,14 +167,27 @@ const TopNav = ({ ]); useEffect(() => { - if (!vis.data.indexPattern) { - services.data.indexPatterns.getDefault().then((index) => { - if (index) { - setIndexPattern(index); + const asyncSetIndexPattern = async () => { + let indexes: IndexPattern[] | undefined; + + if (vis.type.getUsedIndexPattern) { + indexes = await vis.type.getUsedIndexPattern(vis.params); + } + if (!indexes || !indexes.length) { + const defaultIndex = await services.data.indexPatterns.getDefault(); + if (defaultIndex) { + indexes = [defaultIndex]; } - }); + } + if (indexes) { + setIndexPatterns(indexes); + } + }; + + if (!vis.data.indexPattern) { + asyncSetIndexPattern(); } - }, [services.data.indexPatterns, vis.data.indexPattern]); + }, [vis.params, vis.type, services.data.indexPatterns, vis.data.indexPattern]); return isChromeVisible ? ( /** @@ -189,7 +204,7 @@ const TopNav = ({ onQuerySubmit={handleRefresh} savedQueryId={currentAppState.savedQuery} onSavedQueryIdChange={stateContainer.transitions.updateSavedQuery} - indexPatterns={indexPattern ? [indexPattern] : undefined} + indexPatterns={indexPatterns} screenTitle={vis.title} showAutoRefreshOnly={!showDatePicker()} showDatePicker={showDatePicker()} @@ -207,7 +222,7 @@ const TopNav = ({ Date: Tue, 1 Dec 2020 11:28:45 +0100 Subject: [PATCH 007/107] SavedObjectsRepository.incrementCounter supports array of fields (#84326) * SavedObjectsRepository.incrementCounter supports array of fields * Fix TS errors * Fix failing test * Ensure all the remarks make it into our documentation * SavedObjectsRepository.incrementCounter initialize option * Move usage collection-specific docs out of repository into usage collection plugins readme * Update api docs * Polish generated docs --- ...jectsincrementcounteroptions.initialize.md | 13 ++ ...ver.savedobjectsincrementcounteroptions.md | 5 +- ...ncrementcounteroptions.migrationversion.md | 2 + ...dobjectsincrementcounteroptions.refresh.md | 2 +- ...savedobjectsrepository.incrementcounter.md | 41 ++++- ...ugin-core-server.savedobjectsrepository.md | 2 +- .../lib/integration_tests/repository.test.ts | 152 ++++++++++++++++++ .../service/lib/repository.test.js | 58 ++++--- .../saved_objects/service/lib/repository.ts | 86 +++++++--- src/core/server/server.api.md | 3 +- .../data/server/kql_telemetry/route.ts | 2 +- .../services/sample_data/usage/usage.ts | 4 +- src/plugins/usage_collection/README.md | 93 ++++++++++- .../server/report/store_report.test.ts | 2 +- .../server/report/store_report.ts | 2 +- .../validation_telemetry_service.ts | 2 +- .../server/collectors/lib/telemetry.test.ts | 2 +- .../server/collectors/lib/telemetry.ts | 2 +- .../lib/telemetry/es_ui_open_apis.test.ts | 6 +- .../server/lib/telemetry/es_ui_open_apis.ts | 8 +- .../lib/telemetry/es_ui_reindex_apis.test.ts | 8 +- .../lib/telemetry/es_ui_reindex_apis.ts | 8 +- 22 files changed, 420 insertions(+), 83 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md create mode 100644 src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md new file mode 100644 index 0000000000000..61091306d0dbc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) > [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) + +## SavedObjectsIncrementCounterOptions.initialize property + +(default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. + +Signature: + +```typescript +initialize?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index 6077945ddd376..68e9bb09456cd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt | Property | Type | Description | | --- | --- | --- | -| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | | -| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | +| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | +| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md index 417db99fd5a27..aff80138d61cf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsIncrementCounterOptions.migrationVersion property +[SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md index 31d957ca30a3e..4f217cc223d46 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md @@ -4,7 +4,7 @@ ## SavedObjectsIncrementCounterOptions.refresh property -The Elasticsearch Refresh setting for this operation +(default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index f3a2ee38cbdbd..dc62cacf6741b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -4,26 +4,53 @@ ## SavedObjectsRepository.incrementCounter() method -Increases a counter field by one. Creates the document if one doesn't exist for the given id. +Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. Signature: ```typescript -incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; +incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| counterFieldName | string | | -| options | SavedObjectsIncrementCounterOptions | | +| type | string | The type of saved object whose fields should be incremented | +| id | string | The id of the document whose fields should be incremented | +| counterFieldNames | string[] | An array of field names to increment | +| options | SavedObjectsIncrementCounterOptions | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | Returns: `Promise` -{promise} +The saved object after the specified fields were incremented + +## Remarks + +When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` + +When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage\_collection/README.md\#tracking-interactions-with-incrementcounter) + +## Example + + +```ts +const repository = coreStart.savedObjects.createInternalRepository(); + +// Initialize all fields to 0 +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + 'stats.apiCalls', + 'stats.sampleDataInstalled', + ], {initialize: true}); + +// Increment the apiCalls field counter +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + 'stats.apiCalls', + ]) + +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 6a56f0bee718b..e0a6b8af5658a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -26,7 +26,7 @@ export declare class SavedObjectsRepository | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | -| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | +| [incrementCounter(type, id, counterFieldNames, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts new file mode 100644 index 0000000000000..2f64776501df0 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InternalCoreStart } from 'src/core/server/internal_types'; +import * as kbnTestServer from '../../../../../test_helpers/kbn_server'; +import { Root } from '../../../../root'; + +const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); +let esServer: kbnTestServer.TestElasticsearchUtils; + +describe('SavedObjectsRepository', () => { + let root: Root; + let start: InternalCoreStart; + + beforeAll(async () => { + esServer = await startES(); + root = kbnTestServer.createRootWithCorePlugins({ + server: { + basePath: '/hello', + }, + }); + + const setup = await root.setup(); + setup.savedObjects.registerType({ + hidden: false, + mappings: { + dynamic: false, + properties: {}, + }, + name: 'test_counter_type', + namespaceType: 'single', + }); + start = await root.start(); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + describe('#incrementCounter', () => { + describe('initialize=false', () => { + it('creates a new document if none exists and sets all counter fields set to 1', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter('test_counter_type', 'counter_1', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_1'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 1, + "stats.api.count2": 1, + "stats.total": 1, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('increments the specified counters of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + + const now = new Date().getTime(); + // Increment counters + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_2'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 2, + "stats.api.count2": 2, + "stats.total": 2, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + }); + describe('initialize=true', () => { + it('creates a new document if none exists and sets all counter fields to 0', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter( + 'test_counter_type', + 'counter_3', + ['stats.api.count', 'stats.api.count2', 'stats.total'], + { initialize: true } + ); + const result = await repository.get('test_counter_type', 'counter_3'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 0, + "stats.api.count2": 0, + "stats.total": 0, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('sets any undefined counter fields to 0 but does not alter existing fields of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_4', [ + 'stats.existing_field', + ]); + + const now = new Date().getTime(); + // Initialize counters + await repository.incrementCounter( + 'test_counter_type', + 'counter_4', + ['stats.existing_field', 'stats.new_field'], + { initialize: true } + ); + const result = await repository.get('test_counter_type', 'counter_4'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.existing_field": 1, + "stats.new_field": 0, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 6f885f17fd82b..8443d1dd07184 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -3272,11 +3272,11 @@ describe('SavedObjectsRepository', () => { describe('#incrementCounter', () => { const type = 'config'; const id = 'one'; - const field = 'buildNum'; + const counterFields = ['buildNum', 'apiCallsCount']; const namespace = 'foo-namespace'; const originId = 'some-origin-id'; - const incrementCounterSuccess = async (type, id, field, options) => { + const incrementCounterSuccess = async (type, id, fields, options) => { const isMultiNamespace = registry.isMultiNamespace(type); if (isMultiNamespace) { const response = getMockGetResponse({ type, id }, options?.namespace); @@ -3295,7 +3295,10 @@ describe('SavedObjectsRepository', () => { type, ...mockTimestampFields, [type]: { - [field]: 8468, + ...fields.reduce((acc, field) => { + acc[field] = 8468; + return acc; + }, {}), defaultIndex: 'logstash-*', }, }, @@ -3303,25 +3306,25 @@ describe('SavedObjectsRepository', () => { }) ); - const result = await savedObjectsRepository.incrementCounter(type, id, field, options); + const result = await savedObjectsRepository.incrementCounter(type, id, fields, options); expect(client.get).toHaveBeenCalledTimes(isMultiNamespace ? 1 : 0); return result; }; describe('client calls', () => { it(`should use the ES update action if type is not multi-namespace`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledTimes(1); }); it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { - await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, counterFields, { namespace }); expect(client.get).toHaveBeenCalledTimes(1); expect(client.update).toHaveBeenCalledTimes(1); }); it(`defaults to a refresh setting of wait_for`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ refresh: 'wait_for', @@ -3331,7 +3334,7 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${namespace}:${type}:${id}`, @@ -3341,7 +3344,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, field); + await incrementCounterSuccess(type, id, counterFields); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, @@ -3351,7 +3354,7 @@ describe('SavedObjectsRepository', () => { }); it(`normalizes options.namespace from 'default' to undefined`, async () => { - await incrementCounterSuccess(type, id, field, { namespace: 'default' }); + await incrementCounterSuccess(type, id, counterFields, { namespace: 'default' }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, @@ -3361,7 +3364,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); + await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, @@ -3370,7 +3373,7 @@ describe('SavedObjectsRepository', () => { ); client.update.mockClear(); - await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, @@ -3389,7 +3392,7 @@ describe('SavedObjectsRepository', () => { it(`throws when options.namespace is '*'`, async () => { await expect( - savedObjectsRepository.incrementCounter(type, id, field, { + savedObjectsRepository.incrementCounter(type, id, counterFields, { namespace: ALL_NAMESPACES_STRING, }) ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); @@ -3398,7 +3401,7 @@ describe('SavedObjectsRepository', () => { it(`throws when type is not a string`, async () => { const test = async (type) => { await expect( - savedObjectsRepository.incrementCounter(type, id, field) + savedObjectsRepository.incrementCounter(type, id, counterFields) ).rejects.toThrowError(`"type" argument must be a string`); expect(client.update).not.toHaveBeenCalled(); }; @@ -3413,23 +3416,24 @@ describe('SavedObjectsRepository', () => { const test = async (field) => { await expect( savedObjectsRepository.incrementCounter(type, id, field) - ).rejects.toThrowError(`"counterFieldName" argument must be a string`); + ).rejects.toThrowError(`"counterFieldNames" argument must be an array of strings`); expect(client.update).not.toHaveBeenCalled(); }; - await test(null); - await test(42); - await test(false); - await test({}); + await test([null]); + await test([42]); + await test([false]); + await test([{}]); + await test([{}, false, 42, null, 'string']); }); it(`throws when type is invalid`, async () => { - await expectUnsupportedTypeError('unknownType', id, field); + await expectUnsupportedTypeError('unknownType', id, counterFields); expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { - await expectUnsupportedTypeError(HIDDEN_TYPE, id, field); + await expectUnsupportedTypeError(HIDDEN_TYPE, id, counterFields); expect(client.update).not.toHaveBeenCalled(); }); @@ -3439,7 +3443,9 @@ describe('SavedObjectsRepository', () => { elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); await expect( - savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, field, { namespace }) + savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, counterFields, { + namespace, + }) ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); expect(client.get).toHaveBeenCalledTimes(1); }); @@ -3452,8 +3458,8 @@ describe('SavedObjectsRepository', () => { it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; - await incrementCounterSuccess(type, id, field, { migrationVersion }); - const attributes = { buildNum: 1 }; // this is added by the incrementCounter function + await incrementCounterSuccess(type, id, counterFields, { migrationVersion }); + const attributes = { buildNum: 1, apiCallsCount: 1 }; // this is added by the incrementCounter function const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; expectMigrationArgs(doc); @@ -3476,6 +3482,7 @@ describe('SavedObjectsRepository', () => { ...mockTimestampFields, config: { buildNum: 8468, + apiCallsCount: 100, defaultIndex: 'logstash-*', }, originId, @@ -3487,7 +3494,7 @@ describe('SavedObjectsRepository', () => { const response = await savedObjectsRepository.incrementCounter( 'config', '6.0.0-alpha1', - 'buildNum', + ['buildNum', 'apiCallsCount'], { namespace: 'foo-namespace', } @@ -3500,6 +3507,7 @@ describe('SavedObjectsRepository', () => { version: mockVersion, attributes: { buildNum: 8468, + apiCallsCount: 100, defaultIndex: 'logstash-*', }, originId, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index d362c02de4915..2f09ad71de558 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -101,8 +101,17 @@ export interface SavedObjectsRepositoryOptions { * @public */ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + /** + * (default=false) If true, sets all the counter fields to 0 if they don't + * already exist. Existing fields will be left as-is and won't be incremented. + */ + initialize?: boolean; + /** {@link SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; - /** The Elasticsearch Refresh setting for this operation */ + /** + * (default='wait_for') The Elasticsearch refresh setting for this + * operation. See {@link MutatingOperationRefreshSetting} + */ refresh?: MutatingOperationRefreshSetting; } @@ -1515,32 +1524,64 @@ export class SavedObjectsRepository { } /** - * Increases a counter field by one. Creates the document if one doesn't exist for the given id. + * Increments all the specified counter fields by one. Creates the document + * if one doesn't exist for the given id. * - * @param {string} type - * @param {string} id - * @param {string} counterFieldName - * @param {object} [options={}] - * @property {object} [options.migrationVersion=undefined] - * @returns {promise} + * @remarks + * When supplying a field name like `stats.api.counter` the field name will + * be used as-is to create a document like: + * `{attributes: {'stats.api.counter': 1}}` + * It will not create a nested structure like: + * `{attributes: {stats: {api: {counter: 1}}}}` + * + * When using incrementCounter for collecting usage data, you need to ensure + * that usage collection happens on a best-effort basis and doesn't + * negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.md#tracking-interactions-with-incrementcounter) + * + * @example + * ```ts + * const repository = coreStart.savedObjects.createInternalRepository(); + * + * // Initialize all fields to 0 + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * 'stats.apiCalls', + * 'stats.sampleDataInstalled', + * ], {initialize: true}); + * + * // Increment the apiCalls field counter + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * 'stats.apiCalls', + * ]) + * ``` + * + * @param type - The type of saved object whose fields should be incremented + * @param id - The id of the document whose fields should be incremented + * @param counterFieldNames - An array of field names to increment + * @param options - {@link SavedObjectsIncrementCounterOptions} + * @returns The saved object after the specified fields were incremented */ async incrementCounter( type: string, id: string, - counterFieldName: string, + counterFieldNames: string[], options: SavedObjectsIncrementCounterOptions = {} ): Promise { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); } - if (typeof counterFieldName !== 'string') { - throw new Error('"counterFieldName" argument must be a string'); + const isArrayOfStrings = + Array.isArray(counterFieldNames) && + !counterFieldNames.some((field) => typeof field !== 'string'); + if (!isArrayOfStrings) { + throw new Error('"counterFieldNames" argument must be an array of strings'); } if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING } = options; + const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options; const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); @@ -1558,7 +1599,10 @@ export class SavedObjectsRepository { type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: { [counterFieldName]: 1 }, + attributes: counterFieldNames.reduce((acc, counterFieldName) => { + acc[counterFieldName] = initialize ? 0 : 1; + return acc; + }, {} as Record), migrationVersion, updated_at: time, }); @@ -1573,20 +1617,22 @@ export class SavedObjectsRepository { body: { script: { source: ` - if (ctx._source[params.type][params.counterFieldName] == null) { - ctx._source[params.type][params.counterFieldName] = params.count; - } - else { - ctx._source[params.type][params.counterFieldName] += params.count; + for (counterFieldName in params.counterFieldNames) { + if (ctx._source[params.type][counterFieldName] == null) { + ctx._source[params.type][counterFieldName] = params.count; + } + else { + ctx._source[params.type][counterFieldName] += params.count; + } } ctx._source.updated_at = params.time; `, lang: 'painless', params: { - count: 1, + count: initialize ? 0 : 1, time, type, - counterFieldName, + counterFieldNames, }, }, upsert: raw._source, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 8dddff07a0e4c..36a8d9a52fd52 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2369,6 +2369,7 @@ export interface SavedObjectsImportUnsupportedTypeError { // @public (undocumented) export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + initialize?: boolean; // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; refresh?: MutatingOperationRefreshSetting; @@ -2447,7 +2448,7 @@ export class SavedObjectsRepository { // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; + incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/plugins/data/server/kql_telemetry/route.ts b/src/plugins/data/server/kql_telemetry/route.ts index efcb3d038bcc6..c93500f360ad0 100644 --- a/src/plugins/data/server/kql_telemetry/route.ts +++ b/src/plugins/data/server/kql_telemetry/route.ts @@ -45,7 +45,7 @@ export function registerKqlTelemetryRoute( const counterName = optIn ? 'optInCount' : 'optOutCount'; try { - await internalRepository.incrementCounter('kql-telemetry', 'kql-telemetry', counterName); + await internalRepository.incrementCounter('kql-telemetry', 'kql-telemetry', [counterName]); } catch (error) { logger.warn(`Unable to increment counter: ${error}`); return response.customError({ diff --git a/src/plugins/home/server/services/sample_data/usage/usage.ts b/src/plugins/home/server/services/sample_data/usage/usage.ts index ba67906febf1a..6a243b47dee55 100644 --- a/src/plugins/home/server/services/sample_data/usage/usage.ts +++ b/src/plugins/home/server/services/sample_data/usage/usage.ts @@ -43,7 +43,7 @@ export function usage( addInstall: async (dataSet: string) => { try { const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `installCount`); + await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, [`installCount`]); } catch (err) { handleIncrementError(err); } @@ -51,7 +51,7 @@ export function usage( addUninstall: async (dataSet: string) => { try { const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `unInstallCount`); + await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, [`unInstallCount`]); } catch (err) { handleIncrementError(err); } diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 5e6ed901c7647..33f7993f14233 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -140,6 +140,98 @@ export function registerMyPluginUsageCollector( } ``` +## Tracking interactions with incrementCounter +There are several ways to collect data that can provide insight into how users +use your plugin or specific features. For tracking user interactions the +`SavedObjectsRepository` provided by Core provides a useful `incrementCounter` +method which can be used to increment one or more counter fields in a +document. Examples of interactions include tracking: + - the number of API calls + - the number of times users installed and uninstalled the sample datasets + +When using `incrementCounter` for collecting usage data, you need to ensure +that usage collection happens on a best-effort basis and doesn't +negatively affect your plugin or users (see the example): + - Swallow any exceptions thrown from the incrementCounter method and log + a message in development. + - Don't block your application on the incrementCounter method (e.g. + don't use `await`) + - Set the `refresh` option to false to prevent unecessary index refreshes + which slows down Elasticsearch performance + + +Note: for brevity the following example does not follow Kibana's conventions +for structuring your plugin code. +```ts +// src/plugins/dashboard/server/plugin.ts + +import { PluginInitializerContext, Plugin, CoreStart, CoreSetup } from '../../src/core/server'; + +export class DashboardPlugin implements Plugin { + private readonly logger: Logger; + private readonly isDevEnvironment: boolean; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.isDevEnvironment = initializerContext.env.cliArgs.dev; + } + public setup(core) { + // Register a saved object type to store our usage counters + core.savedObjects.registerType({ + // Don't expose this saved object type via the saved objects HTTP API + hidden: true, + mappings: { + // Since we're not querying or aggregating over our counter documents + // we don't define any fields. + dynamic: false, + properties: {}, + }, + name: 'dashboard_usage_counters', + namespaceType: 'single', + }); + } + public start(core) { + const repository = core.savedObjects.createInternalRepository(['dashboard_usage_counters']); + // Initialize all the counter fields to 0 when our plugin starts + // NOTE: Usage collection happens on a best-effort basis, so we don't + // `await` the promise returned by `incrementCounter` and we swallow any + // exceptions in production. + repository + .incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [ + 'apiCalls', + 'settingToggled', + ], {refresh: false, initialize: true}) + .catch((e) => (this.isDevEnvironment ? this.logger.error(e) : e)); + + const router = core.http.createRouter(); + + router.post( + { + path: `api/v1/dashboard/counters/{counter}`, + validate: { + params: schema.object({ + counter: schema.oneOf([schema.literal('apiCalls'), schema.literal('settingToggled')]), + }), + }, + }, + async (context, request, response) => { + request.params.id + + // NOTE: Usage collection happens on a best-effort basis, so we don't + // `await` the promise returned by `incrementCounter` and we swallow any + // exceptions in production. + repository + .incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [ + counter + ], {refresh: false}) + .catch((e) => (this.isDevEnvironement ? this.logger.error(e) : e)); + + return response.ok(); + } + ); + } +} + ## Schema Field The `schema` field is a proscribed data model assists with detecting changes in usage collector payloads. To define the collector schema add a schema field that specifies every possible field reported when registering the collector. Whenever the `schema` field is set or changed please run `node scripts/telemetry_check.js --fix` to update the stored schema json files. @@ -200,7 +292,6 @@ export const myCollector = makeUsageCollector({ }, }); ``` - ## Update the telemetry payload and telemetry cluster field mappings There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. diff --git a/src/plugins/usage_collection/server/report/store_report.test.ts b/src/plugins/usage_collection/server/report/store_report.test.ts index d8327eb834e12..939c37764ab0e 100644 --- a/src/plugins/usage_collection/server/report/store_report.test.ts +++ b/src/plugins/usage_collection/server/report/store_report.test.ts @@ -69,7 +69,7 @@ describe('store_report', () => { expect(savedObjectClient.incrementCounter).toHaveBeenCalledWith( 'ui-metric', 'test-app-name:test-event-name', - 'count' + ['count'] ); expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith([ { diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts index d9aac23fd1ff0..a54d3d226d736 100644 --- a/src/plugins/usage_collection/server/report/store_report.ts +++ b/src/plugins/usage_collection/server/report/store_report.ts @@ -50,7 +50,7 @@ export async function storeReport( const savedObjectId = `${appName}:${eventName}`; return { saved_objects: [ - await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'), + await internalRepository.incrementCounter('ui-metric', savedObjectId, ['count']), ], }; }), diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index 0969174c7143c..46f46eaa3026f 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -83,7 +83,7 @@ export class ValidationTelemetryService implements Plugin { expect(incrementCounterMock).toHaveBeenCalledWith( 'app_search_telemetry', 'app_search_telemetry', - 'ui_clicked.button' + ['ui_clicked.button'] ); expect(response).toEqual({ success: true }); }); diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts index cd8ad72bf8358..deba94fc0bd5e 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts @@ -55,7 +55,7 @@ export async function incrementUICounter({ await internalRepository.incrementCounter( id, id, - `${uiAction}.${metric}` // e.g., ui_viewed.setup_guide + [`${uiAction}.${metric}`] // e.g., ui_viewed.setup_guide ); return { success: true }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts index 703351c45ba5a..af55cc9968b14 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts @@ -29,17 +29,17 @@ describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.overview` + [`ui_open.overview`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.cluster` + [`ui_open.cluster`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.indices` + [`ui_open.indices`] ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts index 64e9b0f217555..45cae937fb466 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts @@ -23,11 +23,9 @@ async function incrementUIOpenOptionCounter({ }: IncrementUIOpenDependencies) { const internalRepository = savedObjects.createInternalRepository(); - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_open.${uiOpenOptionCounter}` - ); + await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ + `ui_open.${uiOpenOptionCounter}`, + ]); } type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts index 31e4e3f07b5de..c157d8860de12 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts @@ -28,22 +28,22 @@ describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.close` + [`ui_reindex.close`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.open` + [`ui_reindex.open`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.start` + [`ui_reindex.start`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.stop` + [`ui_reindex.stop`] ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts index 0aaaf63196d67..4c57b586a46cd 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts @@ -23,11 +23,9 @@ async function incrementUIReindexOptionCounter({ }: IncrementUIReindexOptionDependencies) { const internalRepository = savedObjects.createInternalRepository(); - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.${uiReindexOptionCounter}` - ); + await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ + `ui_reindex.${uiReindexOptionCounter}`, + ]); } type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; From 7dcaff5dddb0389bd2e5b35b41d58006710afc91 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 1 Dec 2020 10:38:28 +0000 Subject: [PATCH 008/107] [Alerting] renames Resolved action group to Recovered (#84123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR changes the default term from “Resolved” to “Recovered”, as it fits most use cases and we feel users are most likely to understand its meaning across domains. --- .../alerts/common/builtin_action_groups.ts | 10 +-- .../alerts/server/alert_type_registry.test.ts | 14 ++--- .../tests/get_alert_instance_summary.test.ts | 2 +- ...rt_instance_summary_from_event_log.test.ts | 62 ++++++++++++++++--- .../alert_instance_summary_from_event_log.ts | 5 +- x-pack/plugins/alerts/server/plugin.ts | 5 +- .../server/task_runner/task_runner.test.ts | 12 ++-- .../alerts/server/task_runner/task_runner.ts | 32 +++++----- .../inventory_metric_threshold_executor.ts | 4 +- .../metric_threshold_executor.test.ts | 6 +- .../metric_threshold_executor.ts | 4 +- .../public/application/constants/index.ts | 6 +- .../action_form.test.tsx | 27 ++++---- .../action_type_form.tsx | 10 +-- .../tests/alerting/list_alert_types.ts | 4 +- .../spaces_only/tests/alerting/alerts_base.ts | 16 ++--- .../spaces_only/tests/alerting/event_log.ts | 14 ++--- .../alerting/get_alert_instance_summary.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- 19 files changed, 147 insertions(+), 90 deletions(-) diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index d31f75357d370..d9c5ae613f787 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,13 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const ResolvedActionGroup: ActionGroup = { - id: 'resolved', - name: i18n.translate('xpack.alerts.builtinActionGroups.resolved', { - defaultMessage: 'Resolved', +export const RecoveredActionGroup: ActionGroup = { + id: 'recovered', + name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { + defaultMessage: 'Recovered', }), }; export function getBuiltinActionGroups(): ActionGroup[] { - return [ResolvedActionGroup]; + return [RecoveredActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 8dc387f96addb..b04871a047e4b 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -105,8 +105,8 @@ describe('register()', () => { name: 'Default', }, { - id: 'resolved', - name: 'Resolved', + id: 'recovered', + name: 'Recovered', }, ], defaultActionGroupId: 'default', @@ -117,7 +117,7 @@ describe('register()', () => { expect(() => registry.register(alertType)).toThrowError( new Error( - `Alert type [id="${alertType.id}"] cannot be registered. Action groups [resolved] are reserved by the framework.` + `Alert type [id="${alertType.id}"] cannot be registered. Action groups [recovered] are reserved by the framework.` ) ); }); @@ -229,8 +229,8 @@ describe('get()', () => { "name": "Default", }, Object { - "id": "resolved", - "name": "Resolved", + "id": "recovered", + "name": "Recovered", }, ], "actionVariables": Object { @@ -287,8 +287,8 @@ describe('list()', () => { "name": "Test Action Group", }, Object { - "id": "resolved", - "name": "Resolved", + "id": "recovered", + "name": "Recovered", }, ], "actionVariables": Object { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 9bd61c0fe66d2..0a764ea768591 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -122,7 +122,7 @@ describe('getAlertInstanceSummary()', () => { .addActiveInstance('instance-previously-active', 'action group B') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-previously-active') + .addRecoveredInstance('instance-previously-active') .addActiveInstance('instance-currently-active', 'action group A') .getEvents(); const eventsResult = { diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index f9e4a2908d6ce..1d5ebe2b5911e 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary } from '../types'; import { IValidatedEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log'; const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; @@ -189,7 +189,43 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-1') + .addRecoveredInstance('instance-1') + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + }); + + test('legacy alert with currently inactive instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addLegacyResolvedInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -224,7 +260,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-1') + .addRecoveredInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -406,7 +442,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addResolvedInstance('instance-2') + .addRecoveredInstance('instance-2') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -451,7 +487,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addResolvedInstance('instance-2') + .addRecoveredInstance('instance-2') .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group B') @@ -561,12 +597,24 @@ export class EventsFactory { return this; } - addResolvedInstance(instanceId: string): EventsFactory { + addRecoveredInstance(instanceId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.recoveredInstance, + }, + kibana: { alerting: { instance_id: instanceId } }, + }); + return this; + } + + addLegacyResolvedInstance(instanceId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.resolvedInstance, + action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, }, kibana: { alerting: { instance_id: instanceId } }, }); diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index 8fed97a74435d..6fed8b4aa4ee6 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types'; import { IEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; export interface AlertInstanceSummaryFromEventLogParams { alert: SanitizedAlert; @@ -80,7 +80,8 @@ export function alertInstanceSummaryFromEventLog( status.status = 'Active'; status.actionGroupId = event?.kibana?.alerting?.action_group_id; break; - case EVENT_LOG_ACTIONS.resolvedInstance: + case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: + case EVENT_LOG_ACTIONS.recoveredInstance: status.status = 'OK'; status.activeStartDate = undefined; status.actionGroupId = undefined; diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 4bfb44425544a..bafb89c64076b 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -82,9 +82,12 @@ export const EVENT_LOG_ACTIONS = { execute: 'execute', executeAction: 'execute-action', newInstance: 'new-instance', - resolvedInstance: 'resolved-instance', + recoveredInstance: 'recovered-instance', activeInstance: 'active-instance', }; +export const LEGACY_EVENT_LOG_ACTIONS = { + resolvedInstance: 'resolved-instance', +}; export interface PluginSetupContract { registerType: AlertTypeRegistry['register']; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 07d08f5837d54..d4c4f746392c3 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -26,12 +26,12 @@ import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { Alert, ResolvedActionGroup } from '../../common'; +import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; const alertType = { id: 'test', name: 'My test alert', - actionGroups: [{ id: 'default', name: 'Default' }, ResolvedActionGroup], + actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], defaultActionGroupId: 'default', executor: jest.fn(), producer: 'alerts', @@ -114,7 +114,7 @@ describe('Task Runner', () => { }, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: '2', actionTypeId: 'action', params: { @@ -517,7 +517,7 @@ describe('Task Runner', () => { `); }); - test('fire resolved actions for execution for the alertInstances which is in the resolved state', async () => { + test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -650,7 +650,7 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "resolved-instance", + "action": "recovered-instance", }, "kibana": Object { "alerting": Object { @@ -666,7 +666,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' resolved instance: '2'", + "message": "test:1: 'alert-name' instance '2' has recovered", }, ], Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 24d96788c3395..5a7247ac50ea0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -39,7 +39,7 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; import { AlertsClient } from '../alerts_client'; import { partiallyUpdateAlert } from '../saved_objects'; -import { ResolvedActionGroup } from '../../common'; +import { RecoveredActionGroup } from '../../common'; const FALLBACK_RETRY_INTERVAL = '5m'; @@ -219,7 +219,7 @@ export class TaskRunner { alertInstance.hasScheduledActions() ); - generateNewAndResolvedInstanceEvents({ + generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, @@ -229,7 +229,7 @@ export class TaskRunner { }); if (!muteAll) { - scheduleActionsForResolvedInstances( + scheduleActionsForRecoveredInstances( alertInstances, executionHandler, originalAlertInstances, @@ -436,7 +436,7 @@ export class TaskRunner { } } -interface GenerateNewAndResolvedInstanceEventsParams { +interface GenerateNewAndRecoveredInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; @@ -445,18 +445,20 @@ interface GenerateNewAndResolvedInstanceEventsParams { namespace: string | undefined; } -function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { +function generateNewAndRecoveredInstanceEvents( + params: GenerateNewAndRecoveredInstanceEventsParams +) { const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); - const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); + const recoveredIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); - for (const id of resolvedIds) { + for (const id of recoveredIds) { const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; - const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); + const message = `${params.alertLabel} instance '${id}' has recovered`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup); } for (const id of newIds) { @@ -496,7 +498,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst } } -function scheduleActionsForResolvedInstances( +function scheduleActionsForRecoveredInstances( alertInstancesMap: Record, executionHandler: ReturnType, originalAlertInstances: Record, @@ -505,22 +507,22 @@ function scheduleActionsForResolvedInstances( ) { const currentAlertInstanceIds = Object.keys(currentAlertInstances); const originalAlertInstanceIds = Object.keys(originalAlertInstances); - const resolvedIds = without( + const recoveredIds = without( originalAlertInstanceIds, ...currentAlertInstanceIds, ...mutedInstanceIds ); - for (const id of resolvedIds) { + for (const id of recoveredIds) { const instance = alertInstancesMap[id]; - instance.updateLastScheduledActions(ResolvedActionGroup.id); + instance.updateLastScheduledActions(RecoveredActionGroup.id); instance.unscheduleActions(); executionHandler({ - actionGroup: ResolvedActionGroup.id, + actionGroup: RecoveredActionGroup.id, context: {}, state: {}, alertInstanceId: id, }); - instance.scheduleActions(ResolvedActionGroup.id); + instance.scheduleActions(RecoveredActionGroup.id); } } diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 14785f64cffac..1941ec6326ddb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; @@ -103,7 +103,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group: item, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index b31afba8ac9cc..a1d6428f3b52b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,7 +6,7 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, @@ -367,7 +367,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('does not continue to send a recovery alert if the metric is still OK', async () => { @@ -383,7 +383,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 7c3918c37ebbf..60790648d9a9b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraBackendLibs } from '../../infra_types'; import { @@ -89,7 +89,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const firstResult = first(alertResults); const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); const actionGroupId = - nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 7af8e5ba88300..156f65f094342 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -16,10 +16,10 @@ export const routeToConnectors = `/connectors`; export const routeToAlerts = `/alerts`; export const routeToAlertDetails = `/alert/:alertId`; -export const resolvedActionGroupMessage = i18n.translate( - 'xpack.triggersActionsUI.sections.actionForm.ResolvedMessage', +export const recoveredActionGroupMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.RecoveredMessage', { - defaultMessage: 'Resolved', + defaultMessage: 'Recovered', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 5b56720737b7e..ddbf933078043 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -10,8 +10,9 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { useKibana } from '../../../common/lib/kibana'; +import { EuiScreenReaderOnly } from '@elastic/eui'; jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -228,7 +229,7 @@ describe('action_form', () => { }} actionGroups={[ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; @@ -347,18 +348,18 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", - "inputDisplay": "Resolved", - "value": "resolved", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "inputDisplay": "Recovered", + "value": "recovered", }, ] `); }); - it('renders selected Resolved action group', async () => { + it('renders selected Recovered action group', async () => { const wrapper = await setup([ { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: 'test', actionTypeId: actionType.id, params: { @@ -381,15 +382,17 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", - "inputDisplay": "Resolved", - "value": "resolved", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "inputDisplay": "Recovered", + "value": "recovered", }, ] `); - expect(actionGroupsSelect.first().text()).toEqual( - 'Select an option: Resolved, is selectedResolved' + + expect(actionGroupsSelect.first().find(EuiScreenReaderOnly).text()).toEqual( + 'Select an option: Recovered, is selected' ); + expect(actionGroupsSelect.first().find('button').first().text()).toEqual('Recovered'); }); it('renders available connectors for the selected action type', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 7e38805957931..fffc3bd32125e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -26,7 +26,7 @@ import { EuiBadge, EuiErrorBoundary, } from '@elastic/eui'; -import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; +import { AlertActionParam, RecoveredActionGroup } from '../../../../../alerts/common'; import { IErrorObject, AlertAction, @@ -40,7 +40,7 @@ import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_en import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; import { transformActionVariables } from '../../lib/action_variables'; -import { resolvedActionGroupMessage } from '../../constants'; +import { recoveredActionGroupMessage } from '../../constants'; import { useKibana } from '../../../common/lib/kibana'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; @@ -105,8 +105,8 @@ export const ActionTypeForm = ({ useEffect(() => { setAvailableActionVariables(getAvailableActionVariables(messageVariables, actionItem.group)); const res = - actionItem.group === ResolvedActionGroup.id - ? resolvedActionGroupMessage + actionItem.group === RecoveredActionGroup.id + ? recoveredActionGroupMessage : defaultActionMessage; setAvailableDefaultActionMessage(res); const paramsDefaults = getDefaultsForActionParams(actionItem.actionTypeId, actionItem.group); @@ -374,7 +374,7 @@ function getAvailableActionVariables( return []; } const filteredActionVariables = - actionGroup === ResolvedActionGroup.id + actionGroup === RecoveredActionGroup.id ? { params: actionVariables.params, state: actionVariables.state } : actionVariables; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index b3635b9f40e27..bfaf8a2a4788e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -17,7 +17,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.noop', @@ -33,7 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedRestrictedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.restricted-noop', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 64e99190e183a..8dab199271da8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { ResolvedActionGroup } from '../../../../../plugins/alerts/common'; +import { RecoveredActionGroup } from '../../../../../plugins/alerts/common'; import { Space } from '../../../common/types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -137,7 +137,7 @@ instanceStateValue: true await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); }); - it('should fire actions when an alert instance is resolved', async () => { + it('should fire actions when an alert instance is recovered', async () => { const reference = alertUtils.generateReference(); const { body: createdAction } = await supertestWithoutAuth @@ -174,12 +174,12 @@ instanceStateValue: true params: {}, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Resolved message', + message: 'Recovered message', }, }, ], @@ -194,10 +194,10 @@ instanceStateValue: true await esTestIndexTool.waitForDocs('action:test.index-record', reference) )[0]; - expect(actionTestRecord._source.params.message).to.eql('Resolved message'); + expect(actionTestRecord._source.params.message).to.eql('Recovered message'); }); - it('should not fire actions when an alert instance is resolved, but alert is muted', async () => { + it('should not fire actions when an alert instance is recovered, but alert is muted', async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); @@ -237,12 +237,12 @@ instanceStateValue: true params: {}, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Resolved message', + message: 'Recovered message', }, }, ], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index d3cd3db124ecd..3766785680925 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -78,7 +78,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { 'execute-action', 'new-instance', 'active-instance', - 'resolved-instance', + 'recovered-instance', ], }); }); @@ -87,25 +87,25 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const executeEvents = getEventsByAction(events, 'execute'); const executeActionEvents = getEventsByAction(events, 'execute-action'); const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const resolvedInstanceEvents = getEventsByAction(events, 'resolved-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); expect(executeEvents.length >= 4).to.be(true); expect(executeActionEvents.length).to.be(2); expect(newInstanceEvents.length).to.be(1); - expect(resolvedInstanceEvents.length).to.be(1); + expect(recoveredInstanceEvents.length).to.be(1); // make sure the events are in the right temporal order const executeTimes = getTimestamps(executeEvents); const executeActionTimes = getTimestamps(executeActionEvents); const newInstanceTimes = getTimestamps(newInstanceEvents); - const resolvedInstanceTimes = getTimestamps(resolvedInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); // validate each event let executeCount = 0; @@ -136,8 +136,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { case 'new-instance': validateInstanceEvent(event, `created new instance: 'instance'`); break; - case 'resolved-instance': - validateInstanceEvent(event, `resolved instance: 'instance'`); + case 'recovered-instance': + validateInstanceEvent(event, `recovered instance: 'instance'`); break; case 'active-instance': validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 22034328e5275..404c6020fa237 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -216,7 +216,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr await alertUtils.muteInstance(createdAlert.id, 'instanceC'); await alertUtils.muteInstance(createdAlert.id, 'instanceD'); - await waitForEvents(createdAlert.id, ['new-instance', 'resolved-instance']); + await waitForEvents(createdAlert.id, ['new-instance', 'recovered-instance']); const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_instance_summary` ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 3fb2cc40437d8..9d38f4abb7f3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -25,7 +25,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType).to.eql({ actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.noop', From 8b2f0cf9d3f3f014f608276212e42d37dc492984 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 1 Dec 2020 12:04:30 +0100 Subject: [PATCH 009/107] [Discover] Fix double fetching of saved search embeddable (#84060) --- .../public/application/embeddable/search_embeddable.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 980e90d0acf20..e592d0b0ec8fd 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -342,12 +342,11 @@ export class SearchEmbeddable if (isFetchRequired) { this.filtersSearchSource!.setField('filter', this.input.filters); this.filtersSearchSource!.setField('query', this.input.query); - - this.fetch(); - this.prevFilters = this.input.filters; this.prevQuery = this.input.query; this.prevTimeRange = this.input.timeRange; + + this.fetch(); } else if (this.searchScope) { // trigger a digest cycle to make sure non-fetch relevant changes are propagated this.searchScope.$applyAsync(); From ba7918a4865e5a23df5435bb75a0a32d1385771e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 1 Dec 2020 13:46:10 +0100 Subject: [PATCH 010/107] [APM] Minor API improvements (#84614) --- .../anomaly_detection_setup_link.tsx | 4 +-- .../app/Settings/anomaly_detection/index.tsx | 4 +-- .../public/components/app/TraceLink/index.tsx | 2 +- .../public/hooks/useAnomalyDetectionJobs.ts | 2 +- .../merge_transaction_group_data.ts | 16 ++++++------ .../apm/server/routes/create_apm_api.ts | 11 ++++---- .../routes/settings/anomaly_detection.ts | 2 +- x-pack/plugins/apm/server/routes/traces.ts | 16 ++++++++++++ .../plugins/apm/server/routes/transaction.ts | 25 ------------------- .../anomaly_detection/no_access_user.ts | 2 +- .../settings/anomaly_detection/read_user.ts | 2 +- .../settings/anomaly_detection/write_user.ts | 2 +- .../anomaly_detection/no_access_user.ts | 2 +- .../settings/anomaly_detection/read_user.ts | 2 +- .../settings/anomaly_detection/write_user.ts | 2 +- 15 files changed, 43 insertions(+), 51 deletions(-) delete mode 100644 x-pack/plugins/apm/server/routes/transaction.ts diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index e6fc80ed7c3b7..a06d6f357c524 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -23,7 +23,7 @@ import { useUrlParams } from '../../hooks/useUrlParams'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { units } from '../../style/variables'; -export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection'>; +export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>; const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; @@ -57,7 +57,7 @@ export function AnomalyDetectionSetupLink() { export function MissingJobsAlert({ environment }: { environment?: string }) { const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => - callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection` }), + callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }), [], { preservePreviousData: false, showToastOnError: false } ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 2cda5fcf85909..9f43f4aa3df91 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -17,7 +17,7 @@ import { LicensePrompt } from '../../../shared/LicensePrompt'; import { useLicense } from '../../../../hooks/useLicense'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection'>; +export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>; const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], @@ -36,7 +36,7 @@ export function AnomalyDetection() { (callApmApi) => { if (canGetJobs) { return callApmApi({ - endpoint: `GET /api/apm/settings/anomaly-detection`, + endpoint: `GET /api/apm/settings/anomaly-detection/jobs`, }); } }, diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 1a41ffe1f606f..c1c9edb01eb8e 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -27,7 +27,7 @@ export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) { (callApmApi) => { if (traceId) { return callApmApi({ - endpoint: 'GET /api/apm/transaction/{traceId}', + endpoint: 'GET /api/apm/traces/{traceId}/root_transaction', params: { path: { traceId, diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts index 5bb36720e7b9b..bef016e5bedd1 100644 --- a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts +++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts @@ -10,7 +10,7 @@ export function useAnomalyDetectionJobs() { return useFetcher( (callApmApi) => callApmApi({ - endpoint: `GET /api/apm/settings/anomaly-detection`, + endpoint: `GET /api/apm/settings/anomaly-detection/jobs`, }), [], { showToastOnError: false } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts index f9266baddaf27..5f53bfa18c468 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts @@ -46,26 +46,26 @@ export function mergeTransactionGroupData({ const timeseriesBuckets = groupBucket?.timeseries.buckets ?? []; return timeseriesBuckets.reduce( - (prev, point) => { + (acc, point) => { return { - ...prev, + ...acc, latency: { - ...prev.latency, - timeseries: prev.latency.timeseries.concat({ + ...acc.latency, + timeseries: acc.latency.timeseries.concat({ x: point.key, y: point.avg_latency.value, }), }, throughput: { - ...prev.throughput, - timeseries: prev.throughput.timeseries.concat({ + ...acc.throughput, + timeseries: acc.throughput.timeseries.concat({ x: point.key, y: point.transaction_count.value / deltaAsMinutes, }), }, errorRate: { - ...prev.errorRate, - timeseries: prev.errorRate.timeseries.concat({ + ...acc.errorRate, + timeseries: acc.errorRate.timeseries.concat({ x: point.key, y: point.transaction_count.value > 0 diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 9334ce60a3f9e..4f7f6320185bf 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -42,8 +42,11 @@ import { } from './settings/apm_indices'; import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; -import { tracesRoute, tracesByIdRoute } from './traces'; -import { transactionByTraceIdRoute } from './transaction'; +import { + tracesRoute, + tracesByIdRoute, + rootTransactionByTraceIdRoute, +} from './traces'; import { correlationsForSlowTransactionsRoute, correlationsForFailedTransactionsRoute, @@ -147,6 +150,7 @@ const createApmApi = () => { // Traces .add(tracesRoute) .add(tracesByIdRoute) + .add(rootTransactionByTraceIdRoute) // Transaction groups .add(transactionGroupsBreakdownRoute) @@ -166,9 +170,6 @@ const createApmApi = () => { .add(serviceNodesLocalFiltersRoute) .add(uiFiltersEnvironmentsRoute) - // Transaction - .add(transactionByTraceIdRoute) - // Service map .add(serviceMapRoute) .add(serviceMapServiceNodeRoute) diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index e7405ad16a63e..49708e4edb732 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -19,7 +19,7 @@ import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute({ - endpoint: 'GET /api/apm/settings/anomaly-detection', + endpoint: 'GET /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:ml:canGetJobs'], }, diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 0c79d391e1fd7..373dc9b8b6ecd 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -11,6 +11,7 @@ import { getTransactionGroupList } from '../lib/transaction_groups'; import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; export const tracesRoute = createRoute({ endpoint: 'GET /api/apm/traces', @@ -44,3 +45,18 @@ export const tracesByIdRoute = createRoute({ return getTrace(context.params.path.traceId, setup); }, }); + +export const rootTransactionByTraceIdRoute = createRoute({ + endpoint: 'GET /api/apm/traces/{traceId}/root_transaction', + params: t.type({ + path: t.type({ + traceId: t.string, + }), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const { traceId } = context.params.path; + const setup = await setupRequest(context, request); + return getRootTransactionByTraceId(traceId, setup); + }, +}); diff --git a/x-pack/plugins/apm/server/routes/transaction.ts b/x-pack/plugins/apm/server/routes/transaction.ts deleted file mode 100644 index 3294d2e9a8227..0000000000000 --- a/x-pack/plugins/apm/server/routes/transaction.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; -import { createRoute } from './create_route'; - -export const transactionByTraceIdRoute = createRoute({ - endpoint: 'GET /api/apm/transaction/{traceId}', - params: t.type({ - path: t.type({ - traceId: t.string, - }), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const { traceId } = context.params.path; - const setup = await setupRequest(context, request); - return getRootTransactionByTraceId(traceId, setup); - }, -}); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts index 33b7675c92d48..5630bd195b6cd 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const noAccessUser = getService('supertestAsNoAccessUser'); function getAnomalyDetectionJobs() { - return noAccessUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createAnomalyDetectionJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts index a9e6eae8bed88..30e097e791eaa 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmReadUser = getService('supertestAsApmReadUser'); function getAnomalyDetectionJobs() { - return apmReadUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createAnomalyDetectionJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts index 4fa3e46430e91..15659229a1917 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmWriteUser = getService('supertestAsApmWriteUser'); function getAnomalyDetectionJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createAnomalyDetectionJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts index b8f93fd350434..a917bdb3cea23 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const noAccessUser = getService('supertestAsNoAccessUser'); function getJobs() { - return noAccessUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts index edb649f501d39..2265c4dc0a41d 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmReadUser = getService('supertestAsApmReadUser'); function getJobs() { - return apmReadUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts index d257fe1dd0b00..720d66e1efcc8 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmWriteUser = getService('supertestAsApmWriteUser'); function getJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createJobs(environments: string[]) { From 39ceadd96d979196a60677913394075e65ee2c02 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 1 Dec 2020 07:55:48 -0500 Subject: [PATCH 011/107] [Fleet] Fix initialization of history instance provided to react-router (#84585) --- .../plugins/fleet/public/applications/fleet/app.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 766ad961674af..ed91c1cb1479c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -186,16 +186,9 @@ export const FleetAppContext: React.FC<{ /** For testing purposes only */ routerHistory?: History; }> = memo( - ({ - children, - startServices, - config, - history, - kibanaVersion, - extensions, - routerHistory = createHashHistory(), - }) => { + ({ children, startServices, config, history, kibanaVersion, extensions, routerHistory }) => { const isDarkMode = useObservable(startServices.uiSettings.get$('theme:darkMode')); + const [routerHistoryInstance] = useState(routerHistory || createHashHistory()); return ( @@ -207,7 +200,7 @@ export const FleetAppContext: React.FC<{ - + {children} From 4cb44d9e33953f08c7602e0b21763f2a5e4400f3 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 1 Dec 2020 14:01:46 +0100 Subject: [PATCH 012/107] [Search] Integrate "Send to background" UI with session service (#83073) --- ...plugin-plugins-data-public.isearchsetup.md | 3 +- ...lugins-data-public.isearchsetup.session.md | 2 +- ...data-public.isearchsetup.sessionsclient.md | 13 + ...plugin-plugins-data-public.isearchstart.md | 3 +- ...lugins-data-public.isearchstart.session.md | 2 +- ...data-public.isearchstart.sessionsclient.md | 13 + ...gin-plugins-data-public.isessionsclient.md | 11 + ...ugins-data-public.isessionservice.clear.md | 13 - ...gins-data-public.isessionservice.delete.md | 13 - ...lugins-data-public.isessionservice.find.md | 13 - ...plugins-data-public.isessionservice.get.md | 13 - ...data-public.isessionservice.getsession_.md | 13 - ...ata-public.isessionservice.getsessionid.md | 13 - ...s-data-public.isessionservice.isrestore.md | 13 - ...ns-data-public.isessionservice.isstored.md | 13 - ...gin-plugins-data-public.isessionservice.md | 22 +- ...ins-data-public.isessionservice.restore.md | 13 - ...lugins-data-public.isessionservice.save.md | 13 - ...ugins-data-public.isessionservice.start.md | 13 - ...gins-data-public.isessionservice.update.md | 13 - .../kibana-plugin-plugins-data-public.md | 5 +- ...hsessionrestorationinfoprovider.getname.md | 13 + ...orationinfoprovider.geturlgeneratordata.md | 15 ++ ...ic.searchsessionrestorationinfoprovider.md | 21 ++ ...plugin-plugins-data-public.sessionstate.md | 26 ++ .../application/dashboard_app_controller.tsx | 43 +++- .../dashboard/public/application/lib/index.ts | 1 + .../application/lib/session_restoration.ts | 66 +++++ .../dashboard/public/url_generator.test.ts | 33 +++ src/plugins/dashboard/public/url_generator.ts | 13 + .../data/common/search/aggs/agg_type.ts | 8 +- .../data/common/search/aggs/buckets/terms.ts | 9 +- .../data/common/search/session/types.ts | 75 +----- src/plugins/data/kibana.json | 3 +- src/plugins/data/public/index.ts | 8 +- src/plugins/data/public/public.api.md | 87 ++++--- .../expressions/esaggs/request_handler.ts | 3 +- src/plugins/data/public/search/index.ts | 10 +- src/plugins/data/public/search/mocks.ts | 4 +- .../public/search/search_interceptor.test.ts | 96 ++++++- .../data/public/search/search_interceptor.ts | 28 +- .../data/public/search/search_service.test.ts | 4 + .../data/public/search/search_service.ts | 13 +- .../search/session/index.ts} | 4 +- .../search/session/mocks.ts | 27 +- .../{ => session}/session_service.test.ts | 45 +++- .../public/search/session/session_service.ts | 242 ++++++++++++++++++ .../search/session/session_state.test.ts | 124 +++++++++ .../public/search/session/session_state.ts | 234 +++++++++++++++++ .../public/search/session/sessions_client.ts | 91 +++++++ .../data/public/search/session_service.ts | 149 ----------- src/plugins/data/public/search/types.ts | 17 +- .../saved_objects/background_session.ts | 6 + .../data/server/search/routes/session.ts | 14 +- .../search/session/session_service.test.ts | 14 +- .../server/search/session/session_service.ts | 16 +- .../public/application/angular/discover.js | 24 +- .../application/angular/discover_state.ts | 60 ++++- .../discover/public/url_generator.test.ts | 13 + src/plugins/discover/public/url_generator.ts | 34 ++- src/plugins/embeddable/public/public.api.md | 4 + x-pack/plugins/data_enhanced/public/plugin.ts | 1 + .../public/search/search_interceptor.test.ts | 165 +++++++++++- .../public/search/search_interceptor.ts | 32 ++- .../background_session_indicator.stories.tsx | 12 +- .../background_session_indicator.test.tsx | 44 ++-- .../background_session_indicator.tsx | 84 ++++-- ...cted_background_session_indicator.test.tsx | 18 +- ...connected_background_session_indicator.tsx | 37 ++- .../index.ts | 1 - .../components/alerts_table/actions.test.tsx | 2 +- .../dashboard/async_search/async_search.ts | 63 +++-- x-pack/test/functional/config.js | 1 + x-pack/test/functional/services/data/index.ts | 7 + .../services/data/send_to_background.ts | 88 +++++++ x-pack/test/functional/services/index.ts | 2 + 76 files changed, 1908 insertions(+), 576 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md create mode 100644 src/plugins/dashboard/public/application/lib/session_restoration.ts rename src/plugins/data/{common/mocks.ts => public/search/session/index.ts} (78%) rename src/plugins/data/{common => public}/search/session/mocks.ts (67%) rename src/plugins/data/public/search/{ => session}/session_service.test.ts (53%) create mode 100644 src/plugins/data/public/search/session/session_service.ts create mode 100644 src/plugins/data/public/search/session/session_state.test.ts create mode 100644 src/plugins/data/public/search/session/session_state.ts create mode 100644 src/plugins/data/public/search/session/sessions_client.ts delete mode 100644 src/plugins/data/public/search/session_service.ts create mode 100644 x-pack/test/functional/services/data/index.ts create mode 100644 x-pack/test/functional/services/data/send_to_background.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index b2f8e83d8e654..a370c67f460f4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,6 +17,7 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | -| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md index 739fdfdeb5fc3..451dbc86b86b6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -4,7 +4,7 @@ ## ISearchSetup.session property -session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) +Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md new file mode 100644 index 0000000000000..d9af202cf1018 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) + +## ISearchSetup.sessionsClient property + +Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) + +Signature: + +```typescript +sessionsClient: ISessionsClient; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index dba60c7bdf147..a27e155dda111 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,6 +19,7 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | -| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md index 1ad194a9bec86..892b0fa6acb60 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -4,7 +4,7 @@ ## ISearchStart.session property -session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) +Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md new file mode 100644 index 0000000000000..9c3210d2ec417 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) + +## ISearchStart.sessionsClient property + +Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) + +Signature: + +```typescript +sessionsClient: ISessionsClient; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md new file mode 100644 index 0000000000000..d6efabb1b9518 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) + +## ISessionsClient type + +Signature: + +```typescript +export declare type ISessionsClient = PublicContract; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md deleted file mode 100644 index fc3d214eb4cad..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) - -## ISessionService.clear property - -Clears the active session. - -Signature: - -```typescript -clear: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md deleted file mode 100644 index eabb966160c4d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) - -## ISessionService.delete property - -Deletes a session - -Signature: - -```typescript -delete: (sessionId: string) => Promise; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md deleted file mode 100644 index 58e2fea0e6fe9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) - -## ISessionService.find property - -Gets a list of saved sessions - -Signature: - -```typescript -find: (options: SearchSessionFindOptions) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md deleted file mode 100644 index a2dff2f18253b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) - -## ISessionService.get property - -Gets a saved session - -Signature: - -```typescript -get: (sessionId: string) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md deleted file mode 100644 index e30c89fb1a9fd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) - -## ISessionService.getSession$ property - -Returns the observable that emits an update every time the session ID changes - -Signature: - -```typescript -getSession$: () => Observable; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md deleted file mode 100644 index 838023ff1d8b9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) - -## ISessionService.getSessionId property - -Returns the active session ID - -Signature: - -```typescript -getSessionId: () => string | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md deleted file mode 100644 index 8d8cd1f31bb95..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) - -## ISessionService.isRestore property - -Whether the active session is restored (i.e. reusing previous search IDs) - -Signature: - -```typescript -isRestore: () => boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md deleted file mode 100644 index db737880bb84e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) - -## ISessionService.isStored property - -Whether the active session is already saved (i.e. sent to background) - -Signature: - -```typescript -isStored: () => boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md index 02c0a821e552d..8938c880a0471 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -2,28 +2,10 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) -## ISessionService interface +## ISessionService type Signature: ```typescript -export interface ISessionService +export declare type ISessionService = PublicContract; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | -| [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) | (sessionId: string) => Promise<void> | Deletes a session | -| [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) | (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>> | Gets a list of saved sessions | -| [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Gets a saved session | -| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | -| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | -| [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) | () => boolean | Whether the active session is restored (i.e. reusing previous search IDs) | -| [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) | () => boolean | Whether the active session is already saved (i.e. sent to background) | -| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Restores existing session | -| [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) | (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Saves a session | -| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | -| [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) | (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any> | Updates a session | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md deleted file mode 100644 index 96106a6ef7e2d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) - -## ISessionService.restore property - -Restores existing session - -Signature: - -```typescript -restore: (sessionId: string) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md deleted file mode 100644 index 4ac4a96614467..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) - -## ISessionService.save property - -Saves a session - -Signature: - -```typescript -save: (name: string, url: string) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md deleted file mode 100644 index 9e14c5ed26765..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) - -## ISessionService.start property - -Starts a new session - -Signature: - -```typescript -start: () => string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md deleted file mode 100644 index 5e2ff53d58ab7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) - -## ISessionService.update property - -Updates a session - -Signature: - -```typescript -update: (sessionId: string, attributes: Partial) => Promise; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index b8e45cde3c18b..9121b0aade470 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -34,6 +34,7 @@ | [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.kbn_field_types.md) | \* | | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | +| [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) | Possible state that current session can be in | | [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | | | [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | | @@ -74,7 +75,6 @@ | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | | [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | -| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | @@ -89,6 +89,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | @@ -166,6 +167,8 @@ | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | +| [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | +| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KibanaContext](./kibana-plugin-plugins-data-public.kibanacontext.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md new file mode 100644 index 0000000000000..0f0b616066dd6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) + +## SearchSessionInfoProvider.getName property + +User-facing name of the session. e.g. will be displayed in background sessions management list + +Signature: + +```typescript +getName: () => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md new file mode 100644 index 0000000000000..207adaf2bd50b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) + +## SearchSessionInfoProvider.getUrlGeneratorData property + +Signature: + +```typescript +getUrlGeneratorData: () => Promise<{ + urlGeneratorId: ID; + initialState: UrlGeneratorStateMapping[ID]['State']; + restoreState: UrlGeneratorStateMapping[ID]['State']; + }>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md new file mode 100644 index 0000000000000..a3d294f5e3303 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) + +## SearchSessionInfoProvider interface + +Provide info about current search session to be stored in backgroundSearch saved object + +Signature: + +```typescript +export interface SearchSessionInfoProvider +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md new file mode 100644 index 0000000000000..9a60a5b2a9f9b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) + +## SessionState enum + +Possible state that current session can be in + +Signature: + +```typescript +export declare enum SessionState +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| BackgroundCompleted | "backgroundCompleted" | Page load completed with background session created. | +| BackgroundLoading | "backgroundLoading" | Search request was sent to the background. The page is loading in background. | +| Canceled | "canceled" | Current session requests where explicitly canceled by user Displaying none or partial results | +| Completed | "completed" | No action was taken and the page completed loading without background session creation. | +| Loading | "loading" | Pending search request has not been sent to the background yet | +| None | "none" | Session is not active, e.g. didn't start | +| Restored | "restored" | Revisiting the page after background completion | + diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index c99e4e4e06987..0d9e7e51b4a97 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -74,7 +74,7 @@ import { NavAction, SavedDashboardPanel } from '../types'; import { showOptionsPopover } from './top_nav/show_options_popover'; import { DashboardSaveModal, SaveOptions } from './top_nav/save_modal'; import { showCloneModal } from './top_nav/show_clone_modal'; -import { saveDashboard } from './lib'; +import { createSessionRestorationDataProvider, saveDashboard } from './lib'; import { DashboardStateManager } from './dashboard_state_manager'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; import { getTopNavConfig } from './top_nav/get_top_nav_config'; @@ -150,7 +150,7 @@ export class DashboardAppController { dashboardCapabilities, scopedHistory, embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, - data: { query: queryService, search: searchService }, + data, core: { notifications, overlays, @@ -168,6 +168,8 @@ export class DashboardAppController { navigation, savedObjectsTagging, }: DashboardAppControllerDependencies) { + const queryService = data.query; + const searchService = data.search; const filterManager = queryService.filterManager; const timefilter = queryService.timefilter.timefilter; const queryStringManager = queryService.queryString; @@ -262,6 +264,16 @@ export class DashboardAppController { $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; + const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; + + const getDashTitle = () => + getDashboardTitle( + dashboardStateManager.getTitle(), + dashboardStateManager.getViewMode(), + dashboardStateManager.getIsDirty(timefilter), + dashboardStateManager.isNew() + ); + const getShouldShowEditHelp = () => !dashboardStateManager.getPanels().length && dashboardStateManager.getIsEditMode() && @@ -429,6 +441,15 @@ export class DashboardAppController { DashboardContainer >(DASHBOARD_CONTAINER_TYPE); + searchService.session.setSearchSessionInfoProvider( + createSessionRestorationDataProvider({ + data, + getDashboardTitle: () => getDashTitle(), + getDashboardId: () => dash.id, + getAppState: () => dashboardStateManager.getAppState(), + }) + ); + if (dashboardFactory) { const searchSessionIdFromURL = getSearchSessionIdFromURL(history); if (searchSessionIdFromURL) { @@ -552,16 +573,6 @@ export class DashboardAppController { filterManager.getFilters() ); - const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; - - const getDashTitle = () => - getDashboardTitle( - dashboardStateManager.getTitle(), - dashboardStateManager.getViewMode(), - dashboardStateManager.getIsDirty(timefilter), - dashboardStateManager.isNew() - ); - // Push breadcrumbs to new header navigation const updateBreadcrumbs = () => { chrome.setBreadcrumbs([ @@ -638,6 +649,13 @@ export class DashboardAppController { } }; + const searchServiceSessionRefreshSubscribtion = searchService.session.onRefresh$.subscribe( + () => { + lastReloadRequestTime = new Date().getTime(); + refreshDashboardContainer(); + } + ); + const updateStateFromSavedQuery = (savedQuery: SavedQuery) => { const allFilters = filterManager.getFilters(); dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters); @@ -1199,6 +1217,7 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + searchServiceSessionRefreshSubscribtion.unsubscribe(); searchService.session.clear(); }); } diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index e9ebe73c3b34d..6741bbbc5d4b1 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -21,3 +21,4 @@ export { saveDashboard } from './save_dashboard'; export { getAppStateDefaults } from './get_app_state_defaults'; export { migrateAppState } from './migrate_app_state'; export { getDashboardIdFromUrl } from './url'; +export { createSessionRestorationDataProvider } from './session_restoration'; diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts new file mode 100644 index 0000000000000..f8ea8f8dcd76d --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState } from '../../url_generator'; +import { DataPublicPluginStart } from '../../../../data/public'; +import { DashboardAppState } from '../../types'; + +export function createSessionRestorationDataProvider(deps: { + data: DataPublicPluginStart; + getAppState: () => DashboardAppState; + getDashboardTitle: () => string; + getDashboardId: () => string; +}) { + return { + getName: async () => deps.getDashboardTitle(), + getUrlGeneratorData: async () => { + return { + urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, + initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), + restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + }; + }, + }; +} + +function getUrlGeneratorState({ + data, + getAppState, + getDashboardId, + forceAbsoluteTime, // TODO: not implemented +}: { + data: DataPublicPluginStart; + getAppState: () => DashboardAppState; + getDashboardId: () => string; + forceAbsoluteTime: boolean; +}): DashboardUrlGeneratorState { + const appState = getAppState(); + return { + dashboardId: getDashboardId(), + timeRange: data.query.timefilter.timefilter.getTime(), + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.formatQuery(appState.query), + savedQuery: appState.savedQuery, + useHash: false, + preserveSavedFilters: false, + viewMode: appState.viewMode, + panels: getDashboardId() ? undefined : appState.panels, + searchSessionId: data.search.session.getSessionId(), + }; +} diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 461caedc5cba7..0272e9d3ebdf7 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -142,6 +142,39 @@ describe('dashboard url generator', () => { ); }); + test('savedQuery', async () => { + const generator = createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) + ); + const url = await generator.createUrl!({ + savedQuery: '__savedQueryId__', + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` + ); + expect(url).toContain('__savedQueryId__'); + }); + + test('panels', async () => { + const generator = createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) + ); + const url = await generator.createUrl!({ + panels: [{ fakePanelContent: 'fakePanelContent' } as any], + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` + ); + }); + test('if no useHash setting is given, uses the one was start services', async () => { const generator = createDashboardUrlGenerator(() => Promise.resolve({ diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index b23b26e4022dd..182020d032e4e 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -30,6 +30,7 @@ import { UrlGeneratorsDefinition } from '../../share/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; +import { SavedDashboardPanel } from '../common/types'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -86,6 +87,16 @@ export interface DashboardUrlGeneratorState { * (Background search) */ searchSessionId?: string; + + /** + * List of dashboard panels + */ + panels?: SavedDashboardPanel[]; + + /** + * Saved query ID + */ + savedQuery?: string; } export const createDashboardUrlGenerator = ( @@ -137,6 +148,8 @@ export const createDashboardUrlGenerator = ( query: state.query, filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), viewMode: state.viewMode, + panels: state.panels, + savedQuery: state.savedQuery, }), { useHash }, `${appBasePath}#/${hash}` diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 4f4a593764b1e..bf6fe11f746f9 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -55,7 +55,8 @@ export interface AggTypeConfig< aggConfig: TAggConfig, searchSource: ISearchSource, inspectorRequestAdapter?: RequestAdapter, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, + searchSessionId?: string ) => Promise; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; @@ -182,6 +183,8 @@ export class AggType< * @param searchSourceAggs - SearchSource aggregation configuration * @param resp - Response to the main request * @param nestedSearchSource - the new SearchSource that will be used to make post flight request + * @param abortSignal - `AbortSignal` to abort the request + * @param searchSessionId - searchSessionId to be used for grouping requests into a single search session * @return {Promise} */ postFlightRequest: ( @@ -190,7 +193,8 @@ export class AggType< aggConfig: TAggConfig, searchSource: ISearchSource, inspectorRequestAdapter?: RequestAdapter, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, + searchSessionId?: string ) => Promise; /** * Get the serialized format for the values produced by this agg type, diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index ac65e7fa813b3..7071d9c1dc9c4 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -102,7 +102,8 @@ export const getTermsBucketAgg = () => aggConfig, searchSource, inspectorRequestAdapter, - abortSignal + abortSignal, + searchSessionId ) => { if (!resp.aggregations) return resp; const nestedSearchSource = searchSource.createChild(); @@ -124,6 +125,7 @@ export const getTermsBucketAgg = () => 'This request counts the number of documents that fall ' + 'outside the criterion of the data buckets.', }), + searchSessionId, } ); nestedSearchSource.getSearchRequestBody().then((body) => { @@ -132,7 +134,10 @@ export const getTermsBucketAgg = () => request.stats(getRequestInspectorStats(nestedSearchSource)); } - const response = await nestedSearchSource.fetch({ abortSignal }); + const response = await nestedSearchSource.fetch({ + abortSignal, + sessionId: searchSessionId, + }); if (request) { request .stats(getResponseInspectorStats(response, nestedSearchSource)) diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index d1ab22057695a..50ca3ca390ece 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -17,82 +17,19 @@ * under the License. */ -import { Observable } from 'rxjs'; -import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; - -export interface ISessionService { - /** - * Returns the active session ID - * @returns The active session ID - */ - getSessionId: () => string | undefined; - /** - * Returns the observable that emits an update every time the session ID changes - * @returns `Observable` - */ - getSession$: () => Observable; - - /** - * Whether the active session is already saved (i.e. sent to background) - */ - isStored: () => boolean; - - /** - * Whether the active session is restored (i.e. reusing previous search IDs) - */ - isRestore: () => boolean; - - /** - * Starts a new session - */ - start: () => string; - - /** - * Restores existing session - */ - restore: (sessionId: string) => Promise>; - - /** - * Clears the active session. - */ - clear: () => void; - - /** - * Saves a session - */ - save: (name: string, url: string) => Promise>; - - /** - * Gets a saved session - */ - get: (sessionId: string) => Promise>; - - /** - * Gets a list of saved sessions - */ - find: ( - options: SearchSessionFindOptions - ) => Promise>; - +export interface BackgroundSessionSavedObjectAttributes { /** - * Updates a session + * User-facing session name to be displayed in session management */ - update: ( - sessionId: string, - attributes: Partial - ) => Promise; - + name: string; /** - * Deletes a session + * App that created the session. e.g 'discover' */ - delete: (sessionId: string) => Promise; -} - -export interface BackgroundSessionSavedObjectAttributes { - name: string; + appId: string; created: string; expires: string; status: string; + urlGeneratorId: string; initialState: Record; restoreState: Record; idMapping: Record; diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 3e4d08c8faa1b..06b083e0ff3aa 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -6,7 +6,8 @@ "requiredPlugins": [ "bfetch", "expressions", - "uiActions" + "uiActions", + "share" ], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e0b0c5a0ea980..1c07b4b99e4c0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -385,6 +385,7 @@ export { SearchRequest, SearchSourceFields, SortDirection, + SessionState, // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, @@ -395,7 +396,12 @@ export { PainlessError, } from './search'; -export type { SearchSource, ISessionService } from './search'; +export type { + SearchSource, + ISessionService, + SearchSessionInfoProvider, + ISessionsClient, +} from './search'; export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 8ceb91269adbe..ad1861cecea0b 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -40,6 +40,7 @@ import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; import { History } from 'history'; import { Href } from 'history'; +import { HttpSetup } from 'kibana/public'; import { IconType } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -62,7 +63,9 @@ import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; +import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import { PopoverAnchorPosition } from '@elastic/eui'; +import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; @@ -82,6 +85,7 @@ import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; +import { StartServicesAccessor } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; import { ToastsSetup } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -1478,6 +1482,7 @@ export interface ISearchSetup { // (undocumented) aggs: AggsSetup; session: ISessionService; + sessionsClient: ISessionsClient; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1493,6 +1498,7 @@ export interface ISearchStart { search: ISearchGeneric; searchSource: ISearchStartSearchSource; session: ISessionService; + sessionsClient: ISessionsClient; // (undocumented) showError: (e: Error) => void; } @@ -1508,25 +1514,17 @@ export interface ISearchStartSearchSource { // @public (undocumented) export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +// Warning: (ae-forgotten-export) The symbol "SessionsClient" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ISessionsClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ISessionsClient = PublicContract; + +// Warning: (ae-forgotten-export) The symbol "SessionService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ISessionService { - clear: () => void; - delete: (sessionId: string) => Promise; - // Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts - find: (options: SearchSessionFindOptions) => Promise>; - get: (sessionId: string) => Promise>; - getSession$: () => Observable; - getSessionId: () => string | undefined; - isRestore: () => boolean; - isStored: () => boolean; - // Warning: (ae-forgotten-export) The symbol "BackgroundSessionSavedObjectAttributes" needs to be exported by the entry point index.d.ts - restore: (sessionId: string) => Promise>; - save: (name: string, url: string) => Promise>; - start: () => string; - update: (sessionId: string, attributes: Partial) => Promise; -} +export type ISessionService = PublicContract; // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2107,6 +2105,7 @@ export class SearchInterceptor { timeoutSignal: AbortSignal; combinedSignal: AbortSignal; cleanup: () => void; + abort: () => void; }; // (undocumented) showError(e: Error): void; @@ -2135,6 +2134,20 @@ export interface SearchInterceptorDeps { // @internal export type SearchRequest = Record; +// Warning: (ae-forgotten-export) The symbol "UrlGeneratorId" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SearchSessionInfoProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface SearchSessionInfoProvider { + getName: () => Promise; + // (undocumented) + getUrlGeneratorData: () => Promise<{ + urlGeneratorId: ID; + initialState: UrlGeneratorStateMapping[ID]['State']; + restoreState: UrlGeneratorStateMapping[ID]['State']; + }>; +} + // @public (undocumented) export class SearchSource { // Warning: (ae-forgotten-export) The symbol "SearchSourceDependencies" needs to be exported by the entry point index.d.ts @@ -2240,6 +2253,17 @@ export class SearchTimeoutError extends KbnError { mode: TimeoutErrorMode; } +// @public +export enum SessionState { + BackgroundCompleted = "backgroundCompleted", + BackgroundLoading = "backgroundLoading", + Canceled = "canceled", + Completed = "completed", + Loading = "loading", + None = "none", + Restored = "restored" +} + // Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2415,22 +2439,23 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:46:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts index 93b5705b821c0..7a27d65267149 100644 --- a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts @@ -182,7 +182,8 @@ export const handleRequest = async ({ agg, requestSearchSource, inspectorAdapters.requests, - abortSignal + abortSignal, + searchSessionId ); } } diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index f6bd46c17192c..2a767d1bf7c0d 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -40,9 +40,15 @@ export { SearchSourceDependencies, SearchSourceFields, SortDirection, - ISessionService, } from '../../common/search'; - +export { + SessionService, + ISessionService, + SearchSessionInfoProvider, + SessionState, + SessionsClient, + ISessionsClient, +} from './session'; export { getEsPreference } from './es_search'; export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 836ddb618e746..b799c661051fa 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -20,13 +20,14 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; import { ISearchSetup, ISearchStart } from './types'; -import { getSessionServiceMock } from '../../common/mocks'; +import { getSessionsClientMock, getSessionServiceMock } from './session/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), __enhance: jest.fn(), session: getSessionServiceMock(), + sessionsClient: getSessionsClientMock(), }; } @@ -36,6 +37,7 @@ function createStartContract(): jest.Mocked { search: jest.fn(), showError: jest.fn(), session: getSessionServiceMock(), + sessionsClient: getSessionsClientMock(), searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 6dc52d7016797..947dac1b32640 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -24,7 +24,7 @@ import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../../kibana_utils/public'; import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; import { searchServiceMock } from './mocks'; -import { ISearchStart } from '.'; +import { ISearchStart, ISessionService } from '.'; import { bfetchPluginMock } from '../../../bfetch/public/mocks'; import { BfetchPublicSetup } from 'src/plugins/bfetch/public'; @@ -104,7 +104,99 @@ describe('SearchInterceptor', () => { params: {}, }; const response = searchInterceptor.search(mockRequest); - expect(response.toPromise()).resolves.toBe(mockResponse); + await expect(response.toPromise()).resolves.toBe(mockResponse); + }); + + describe('Search session', () => { + const setup = ({ + isRestore = false, + isStored = false, + sessionId, + }: { + isRestore?: boolean; + isStored?: boolean; + sessionId?: string; + }) => { + const sessionServiceMock = searchMock.session as jest.Mocked; + sessionServiceMock.getSessionId.mockImplementation(() => sessionId); + sessionServiceMock.isRestore.mockImplementation(() => isRestore); + sessionServiceMock.isStored.mockImplementation(() => isStored); + fetchMock.mockResolvedValue({ result: 200 }); + }; + + const mockRequest: IEsSearchRequest = { + params: {}, + }; + + afterEach(() => { + const sessionServiceMock = searchMock.session as jest.Mocked; + sessionServiceMock.getSessionId.mockReset(); + sessionServiceMock.isRestore.mockReset(); + sessionServiceMock.isStored.mockReset(); + fetchMock.mockReset(); + }); + + test('infers isRestore from session service state', async () => { + const sessionId = 'sid'; + setup({ + isRestore: true, + sessionId, + }); + + await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'sid', isStored: false, isRestore: true }, + }) + ); + }); + + test('infers isStored from session service state', async () => { + const sessionId = 'sid'; + setup({ + isStored: true, + sessionId, + }); + + await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'sid', isStored: true, isRestore: false }, + }) + ); + }); + + test('skips isRestore & isStore in case not a current session Id', async () => { + setup({ + isStored: true, + isRestore: true, + sessionId: 'session id', + }); + + await searchInterceptor + .search(mockRequest, { sessionId: 'different session id' }) + .toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'different session id', isStored: false, isRestore: false }, + }) + ); + }); + + test('skips isRestore & isStore in case no session Id', async () => { + setup({ + isStored: true, + isRestore: true, + sessionId: undefined, + }); + + await searchInterceptor.search(mockRequest, { sessionId: 'sessionId' }).toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'sessionId', isStored: false, isRestore: false }, + }) + ); + }); }); describe('Should throw typed errors', () => { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 055b3a71705bf..8548a2a9f2b2a 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -24,12 +24,7 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { BatchedFunc, BfetchPublicSetup } from 'src/plugins/bfetch/public'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - ISearchOptions, - ISessionService, -} from '../../common'; +import { IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions } from '../../common'; import { SearchUsageCollector } from './collectors'; import { SearchTimeoutError, @@ -42,6 +37,7 @@ import { } from './errors'; import { toMountPoint } from '../../../kibana_react/public'; import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public'; +import { ISessionService } from './session'; export interface SearchInterceptorDeps { bfetch: BfetchPublicSetup; @@ -133,10 +129,18 @@ export class SearchInterceptor { options?: ISearchOptions ): Promise { const { abortSignal, ...requestOptions } = options || {}; + + const isCurrentSession = + options?.sessionId && this.deps.session.getSessionId() === options.sessionId; + return this.batchedFetch( { request, - options: requestOptions, + options: { + ...requestOptions, + isStored: isCurrentSession ? this.deps.session.isStored() : false, + isRestore: isCurrentSession ? this.deps.session.isRestore() : false, + }, }, abortSignal ); @@ -160,13 +164,18 @@ export class SearchInterceptor { timeoutController.abort(); }); + const selfAbortController = new AbortController(); + // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: // 1. The user manually aborts (via `cancelPending`) // 2. The request times out - // 3. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) + // 3. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks + // in the current session + // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) const signals = [ this.abortController.signal, timeoutSignal, + selfAbortController.signal, ...(abortSignal ? [abortSignal] : []), ]; @@ -184,6 +193,9 @@ export class SearchInterceptor { timeoutSignal, combinedSignal, cleanup, + abort: () => { + selfAbortController.abort(); + }, }; } diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 3179da4d03a1a..d617010d13011 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -49,6 +49,8 @@ describe('Search service', () => { expect(setup).toHaveProperty('aggs'); expect(setup).toHaveProperty('usageCollector'); expect(setup).toHaveProperty('__enhance'); + expect(setup).toHaveProperty('sessionsClient'); + expect(setup).toHaveProperty('session'); }); }); @@ -61,6 +63,8 @@ describe('Search service', () => { expect(start).toHaveProperty('aggs'); expect(start).toHaveProperty('search'); expect(start).toHaveProperty('searchSource'); + expect(start).toHaveProperty('sessionsClient'); + expect(start).toHaveProperty('session'); }); }); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index b76b5846d3d93..60d2dfdf866cf 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -28,7 +28,6 @@ import { kibanaContext, kibanaContextFunction, ISearchGeneric, - ISessionService, SearchSourceDependencies, SearchSourceService, } from '../../common/search'; @@ -40,7 +39,7 @@ import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; -import { SessionService } from './session_service'; +import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session'; import { ConfigSchema } from '../../config'; import { SHARD_DELAY_AGG_NAME, @@ -67,6 +66,7 @@ export class SearchService implements Plugin { private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; private sessionService!: ISessionService; + private sessionsClient!: ISessionsClient; constructor(private initializerContext: PluginInitializerContext) {} @@ -76,7 +76,12 @@ export class SearchService implements Plugin { ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); - this.sessionService = new SessionService(this.initializerContext, getStartServices); + this.sessionsClient = new SessionsClient({ http }); + this.sessionService = new SessionService( + this.initializerContext, + getStartServices, + this.sessionsClient + ); /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -115,6 +120,7 @@ export class SearchService implements Plugin { this.searchInterceptor = enhancements.searchInterceptor; }, session: this.sessionService, + sessionsClient: this.sessionsClient, }; } @@ -146,6 +152,7 @@ export class SearchService implements Plugin { this.searchInterceptor.showError(e); }, session: this.sessionService, + sessionsClient: this.sessionsClient, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), }; } diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/data/public/search/session/index.ts similarity index 78% rename from src/plugins/data/common/mocks.ts rename to src/plugins/data/public/search/session/index.ts index dde70b1d07443..ee0121aacad5e 100644 --- a/src/plugins/data/common/mocks.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { getSessionServiceMock } from './search/session/mocks'; +export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service'; +export { SessionState } from './session_state'; +export { SessionsClient, ISessionsClient } from './sessions_client'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts similarity index 67% rename from src/plugins/data/common/search/session/mocks.ts rename to src/plugins/data/public/search/session/mocks.ts index 4604e15e4e93b..0ff99b3080365 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -17,8 +17,20 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; -import { ISessionService } from './types'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { ISessionsClient } from './sessions_client'; +import { ISessionService } from './session_service'; +import { SessionState } from './session_state'; + +export function getSessionsClientMock(): jest.Mocked { + return { + get: jest.fn(), + create: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; +} export function getSessionServiceMock(): jest.Mocked { return { @@ -27,12 +39,15 @@ export function getSessionServiceMock(): jest.Mocked { restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), + state$: new BehaviorSubject(SessionState.None).asObservable(), + setSearchSessionInfoProvider: jest.fn(), + trackSearch: jest.fn((searchDescriptor) => () => {}), + destroy: jest.fn(), + onRefresh$: new Subject(), + refresh: jest.fn(), + cancel: jest.fn(), isStored: jest.fn(), isRestore: jest.fn(), save: jest.fn(), - get: jest.fn(), - find: jest.fn(), - update: jest.fn(), - delete: jest.fn(), }; } diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts similarity index 53% rename from src/plugins/data/public/search/session_service.test.ts rename to src/plugins/data/public/search/session/session_service.test.ts index bcfd06944d983..83c3185ead63e 100644 --- a/src/plugins/data/public/search/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -17,20 +17,27 @@ * under the License. */ -import { SessionService } from './session_service'; -import { ISessionService } from '../../common'; -import { coreMock } from '../../../../core/public/mocks'; +import { SessionService, ISessionService } from './session_service'; +import { coreMock } from '../../../../../core/public/mocks'; import { take, toArray } from 'rxjs/operators'; +import { getSessionsClientMock } from './mocks'; +import { BehaviorSubject } from 'rxjs'; +import { SessionState } from './session_state'; describe('Session service', () => { let sessionService: ISessionService; + let state$: BehaviorSubject; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); sessionService = new SessionService( initializerContext, - coreMock.createSetup().getStartServices + coreMock.createSetup().getStartServices, + getSessionsClientMock(), + { freezeState: false } // needed to use mocks inside state container ); + state$ = new BehaviorSubject(SessionState.None); + sessionService.state$.subscribe(state$); }); describe('Session management', () => { @@ -55,5 +62,35 @@ describe('Session service', () => { expect(await emittedValues).toEqual(['1', '2', undefined]); }); + + it('Tracks searches for current session', () => { + expect(() => sessionService.trackSearch({ abort: () => {} })).toThrowError(); + expect(state$.getValue()).toBe(SessionState.None); + + sessionService.start(); + const untrack1 = sessionService.trackSearch({ abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.Loading); + const untrack2 = sessionService.trackSearch({ abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.Loading); + untrack1(); + expect(state$.getValue()).toBe(SessionState.Loading); + untrack2(); + expect(state$.getValue()).toBe(SessionState.Completed); + }); + + it('Cancels all tracked searches within current session', async () => { + const abort = jest.fn(); + + sessionService.start(); + sessionService.trackSearch({ abort }); + sessionService.trackSearch({ abort }); + sessionService.trackSearch({ abort }); + const untrack = sessionService.trackSearch({ abort }); + + untrack(); + await sessionService.cancel(); + + expect(abort).toBeCalledTimes(3); + }); }); }); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts new file mode 100644 index 0000000000000..ef0b36a33be52 --- /dev/null +++ b/src/plugins/data/public/search/session/session_service.ts @@ -0,0 +1,242 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PublicContract } from '@kbn/utility-types'; +import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; +import { Observable, Subject, Subscription } from 'rxjs'; +import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/'; +import { ConfigSchema } from '../../../config'; +import { createSessionStateContainer, SessionState, SessionStateContainer } from './session_state'; +import { ISessionsClient } from './sessions_client'; + +export type ISessionService = PublicContract; + +export interface TrackSearchDescriptor { + abort: () => void; +} + +/** + * Provide info about current search session to be stored in backgroundSearch saved object + */ +export interface SearchSessionInfoProvider { + /** + * User-facing name of the session. + * e.g. will be displayed in background sessions management list + */ + getName: () => Promise; + getUrlGeneratorData: () => Promise<{ + urlGeneratorId: ID; + initialState: UrlGeneratorStateMapping[ID]['State']; + restoreState: UrlGeneratorStateMapping[ID]['State']; + }>; +} + +/** + * Responsible for tracking a current search session. Supports only a single session at a time. + */ +export class SessionService { + public readonly state$: Observable; + private readonly state: SessionStateContainer; + + private searchSessionInfoProvider?: SearchSessionInfoProvider; + private appChangeSubscription$?: Subscription; + private curApp?: string; + + constructor( + initializerContext: PluginInitializerContext, + getStartServices: StartServicesAccessor, + private readonly sessionsClient: ISessionsClient, + { freezeState = true }: { freezeState: boolean } = { freezeState: true } + ) { + const { stateContainer, sessionState$ } = createSessionStateContainer({ + freeze: freezeState, + }); + this.state$ = sessionState$; + this.state = stateContainer; + + getStartServices().then(([coreStart]) => { + // Apps required to clean up their sessions before unmounting + // Make sure that apps don't leave sessions open. + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { + if (this.state.get().sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + this.clear(); + } + } + this.curApp = appName; + }); + }); + } + + /** + * Set a provider of info about current session + * This will be used for creating a background session saved object + * @param searchSessionInfoProvider + */ + public setSearchSessionInfoProvider( + searchSessionInfoProvider: SearchSessionInfoProvider | undefined + ) { + this.searchSessionInfoProvider = searchSessionInfoProvider; + } + + /** + * Used to track pending searches within current session + * + * @param searchDescriptor - uniq object that will be used to untrack the search + * @returns untrack function + */ + public trackSearch(searchDescriptor: TrackSearchDescriptor): () => void { + this.state.transitions.trackSearch(searchDescriptor); + return () => { + this.state.transitions.unTrackSearch(searchDescriptor); + }; + } + + public destroy() { + if (this.appChangeSubscription$) { + this.appChangeSubscription$.unsubscribe(); + } + this.clear(); + } + + /** + * Get current session id + */ + public getSessionId() { + return this.state.get().sessionId; + } + + /** + * Get observable for current session id + */ + public getSession$() { + return this.state.state$.pipe( + startWith(this.state.get()), + map((s) => s.sessionId), + distinctUntilChanged() + ); + } + + /** + * Is current session already saved as SO (send to background) + */ + public isStored() { + return this.state.get().isStored; + } + + /** + * Is restoring the older saved searches + */ + public isRestore() { + return this.state.get().isRestore; + } + + /** + * Start a new search session + * @returns sessionId + */ + public start() { + this.state.transitions.start(); + return this.getSessionId()!; + } + + /** + * Restore previously saved search session + * @param sessionId + */ + public restore(sessionId: string) { + this.state.transitions.restore(sessionId); + } + + /** + * Cleans up current state + */ + public clear() { + this.state.transitions.clear(); + this.setSearchSessionInfoProvider(undefined); + } + + private refresh$ = new Subject(); + /** + * Observable emits when search result refresh was requested + * For example, search to background UI could have it's own "refresh" button + * Application would use this observable to handle user interaction on that button + */ + public onRefresh$ = this.refresh$.asObservable(); + + /** + * Request a search results refresh + */ + public refresh() { + this.refresh$.next(); + } + + /** + * Request a cancellation of on-going search requests within current session + */ + public async cancel(): Promise { + const isStoredSession = this.state.get().isStored; + this.state.get().pendingSearches.forEach((s) => { + s.abort(); + }); + this.state.transitions.cancel(); + if (isStoredSession) { + await this.sessionsClient.delete(this.state.get().sessionId!); + } + } + + /** + * Save current session as SO to get back to results later + * (Send to background) + */ + public async save(): Promise { + const sessionId = this.getSessionId(); + if (!sessionId) throw new Error('No current session'); + if (!this.curApp) throw new Error('No current app id'); + const currentSessionInfoProvider = this.searchSessionInfoProvider; + if (!currentSessionInfoProvider) throw new Error('No info provider for current session'); + const [name, { initialState, restoreState, urlGeneratorId }] = await Promise.all([ + currentSessionInfoProvider.getName(), + currentSessionInfoProvider.getUrlGeneratorData(), + ]); + + await this.sessionsClient.create({ + name, + appId: this.curApp, + restoreState: (restoreState as unknown) as Record, + initialState: (initialState as unknown) as Record, + urlGeneratorId, + sessionId, + }); + + // if we are still interested in this result + if (this.getSessionId() === sessionId) { + this.state.transitions.store(); + } + } +} diff --git a/src/plugins/data/public/search/session/session_state.test.ts b/src/plugins/data/public/search/session/session_state.test.ts new file mode 100644 index 0000000000000..5f709c75bb5d2 --- /dev/null +++ b/src/plugins/data/public/search/session/session_state.test.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createSessionStateContainer, SessionState } from './session_state'; + +describe('Session state container', () => { + const { stateContainer: state } = createSessionStateContainer(); + + afterEach(() => { + state.transitions.clear(); + }); + + describe('transitions', () => { + test('start', () => { + state.transitions.start(); + expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.get().sessionId).not.toBeUndefined(); + }); + + test('track', () => { + expect(() => state.transitions.trackSearch({})).toThrowError(); + + state.transitions.start(); + state.transitions.trackSearch({}); + + expect(state.selectors.getState()).toBe(SessionState.Loading); + }); + + test('untrack', () => { + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.unTrackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Completed); + }); + + test('clear', () => { + state.transitions.start(); + state.transitions.clear(); + expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.get().sessionId).toBeUndefined(); + }); + + test('cancel', () => { + expect(() => state.transitions.cancel()).toThrowError(); + + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.cancel(); + expect(state.selectors.getState()).toBe(SessionState.Canceled); + state.transitions.clear(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + + test('store -> completed', () => { + expect(() => state.transitions.store()).toThrowError(); + + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.store(); + expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + state.transitions.unTrackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.BackgroundCompleted); + state.transitions.clear(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + test('store -> cancel', () => { + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.store(); + expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + state.transitions.cancel(); + expect(state.selectors.getState()).toBe(SessionState.Canceled); + + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Canceled); + + state.transitions.start(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + + test('restore', () => { + const id = 'id'; + state.transitions.restore(id); + expect(state.selectors.getState()).toBe(SessionState.None); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + state.transitions.unTrackSearch(search); + + expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(() => state.transitions.store()).toThrowError(); + expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(() => state.transitions.cancel()).toThrowError(); + expect(state.selectors.getState()).toBe(SessionState.Restored); + + state.transitions.start(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + }); +}); diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/session_state.ts new file mode 100644 index 0000000000000..087417263e5bf --- /dev/null +++ b/src/plugins/data/public/search/session/session_state.ts @@ -0,0 +1,234 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import uuid from 'uuid'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; +import { createStateContainer, StateContainer } from '../../../../kibana_utils/public'; + +/** + * Possible state that current session can be in + * + * @public + */ +export enum SessionState { + /** + * Session is not active, e.g. didn't start + */ + None = 'none', + + /** + * Pending search request has not been sent to the background yet + */ + Loading = 'loading', + + /** + * No action was taken and the page completed loading without background session creation. + */ + Completed = 'completed', + + /** + * Search request was sent to the background. + * The page is loading in background. + */ + BackgroundLoading = 'backgroundLoading', + + /** + * Page load completed with background session created. + */ + BackgroundCompleted = 'backgroundCompleted', + + /** + * Revisiting the page after background completion + */ + Restored = 'restored', + + /** + * Current session requests where explicitly canceled by user + * Displaying none or partial results + */ + Canceled = 'canceled', +} + +/** + * Internal state of SessionService + * {@link SessionState} is inferred from this state + * + * @private + */ +export interface SessionStateInternal { + /** + * Current session Id + * Empty means there is no current active session. + */ + sessionId?: string; + + /** + * Has the session already been stored (i.e. "sent to background")? + */ + isStored: boolean; + + /** + * Is this session a restored session (have these requests already been made, and we're just + * looking to re-use the previous search IDs)? + */ + isRestore: boolean; + + /** + * Set of currently running searches + * within a session and any info associated with them + */ + pendingSearches: SearchDescriptor[]; + + /** + * There was at least a single search in this session + */ + isStarted: boolean; + + /** + * If user has explicitly canceled search requests + */ + isCanceled: boolean; +} + +const createSessionDefaultState: < + SearchDescriptor = unknown +>() => SessionStateInternal = () => ({ + sessionId: undefined, + isStored: false, + isRestore: false, + isCanceled: false, + isStarted: false, + pendingSearches: [], +}); + +export interface SessionPureTransitions< + SearchDescriptor = unknown, + S = SessionStateInternal +> { + start: (state: S) => () => S; + restore: (state: S) => (sessionId: string) => S; + clear: (state: S) => () => S; + store: (state: S) => () => S; + trackSearch: (state: S) => (search: SearchDescriptor) => S; + unTrackSearch: (state: S) => (search: SearchDescriptor) => S; + cancel: (state: S) => () => S; +} + +export const sessionPureTransitions: SessionPureTransitions = { + start: (state) => () => ({ ...createSessionDefaultState(), sessionId: uuid.v4() }), + restore: (state) => (sessionId: string) => ({ + ...createSessionDefaultState(), + sessionId, + isRestore: true, + isStored: true, + }), + clear: (state) => () => createSessionDefaultState(), + store: (state) => () => { + if (!state.sessionId) throw new Error("Can't store session. Missing sessionId"); + if (state.isStored || state.isRestore) + throw new Error('Can\'t store because current session is already stored"'); + return { + ...state, + isStored: true, + }; + }, + trackSearch: (state) => (search) => { + if (!state.sessionId) throw new Error("Can't track search. Missing sessionId"); + return { + ...state, + isStarted: true, + pendingSearches: state.pendingSearches.concat(search), + }; + }, + unTrackSearch: (state) => (search) => { + return { + ...state, + pendingSearches: state.pendingSearches.filter((s) => s !== search), + }; + }, + cancel: (state) => () => { + if (!state.sessionId) throw new Error("Can't cancel searches. Missing sessionId"); + if (state.isRestore) throw new Error("Can't cancel searches when restoring older searches"); + return { + ...state, + pendingSearches: [], + isCanceled: true, + isStored: false, + }; + }, +}; + +export interface SessionPureSelectors< + SearchDescriptor = unknown, + S = SessionStateInternal +> { + getState: (state: S) => () => SessionState; +} + +export const sessionPureSelectors: SessionPureSelectors = { + getState: (state) => () => { + if (!state.sessionId) return SessionState.None; + if (!state.isStarted) return SessionState.None; + if (state.isCanceled) return SessionState.Canceled; + switch (true) { + case state.isRestore: + return state.pendingSearches.length > 0 + ? SessionState.BackgroundLoading + : SessionState.Restored; + case state.isStored: + return state.pendingSearches.length > 0 + ? SessionState.BackgroundLoading + : SessionState.BackgroundCompleted; + default: + return state.pendingSearches.length > 0 ? SessionState.Loading : SessionState.Completed; + } + return SessionState.None; + }, +}; + +export type SessionStateContainer = StateContainer< + SessionStateInternal, + SessionPureTransitions, + SessionPureSelectors +>; + +export const createSessionStateContainer = ( + { freeze = true }: { freeze: boolean } = { freeze: true } +): { + stateContainer: SessionStateContainer; + sessionState$: Observable; +} => { + const stateContainer = createStateContainer( + createSessionDefaultState(), + sessionPureTransitions, + sessionPureSelectors, + freeze ? undefined : { freeze: (s) => s } + ) as SessionStateContainer; + + const sessionState$: Observable = stateContainer.state$.pipe( + map(() => stateContainer.selectors.getState()), + distinctUntilChanged(), + shareReplay(1) + ); + return { + stateContainer, + sessionState$, + }; +}; diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts new file mode 100644 index 0000000000000..c19c5db064094 --- /dev/null +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PublicContract } from '@kbn/utility-types'; +import { HttpSetup } from 'kibana/public'; +import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { BackgroundSessionSavedObjectAttributes, SearchSessionFindOptions } from '../../../common'; + +export type ISessionsClient = PublicContract; +export interface SessionsClientDeps { + http: HttpSetup; +} + +/** + * CRUD backgroundSession SO + */ +export class SessionsClient { + private readonly http: HttpSetup; + + constructor(deps: SessionsClientDeps) { + this.http = deps.http; + } + + public get(sessionId: string): Promise> { + return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); + } + + public create({ + name, + appId, + urlGeneratorId, + initialState, + restoreState, + sessionId, + }: { + name: string; + appId: string; + initialState: Record; + restoreState: Record; + urlGeneratorId: string; + sessionId: string; + }): Promise> { + return this.http.post(`/internal/session`, { + body: JSON.stringify({ + name, + initialState, + restoreState, + sessionId, + appId, + urlGeneratorId, + }), + }); + } + + public find( + options: SearchSessionFindOptions + ): Promise> { + return this.http!.post(`/internal/session`, { + body: JSON.stringify(options), + }); + } + + public update( + sessionId: string, + attributes: Partial + ): Promise> { + return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, { + body: JSON.stringify(attributes), + }); + } + + public delete(sessionId: string): Promise { + return this.http!.delete(`/internal/session/${encodeURIComponent(sessionId)}`); + } +} diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts deleted file mode 100644 index 0141cff258a9f..0000000000000 --- a/src/plugins/data/public/search/session_service.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import uuid from 'uuid'; -import { BehaviorSubject, Subscription } from 'rxjs'; -import { HttpStart, PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; -import { ConfigSchema } from '../../config'; -import { - ISessionService, - BackgroundSessionSavedObjectAttributes, - SearchSessionFindOptions, -} from '../../common'; - -export class SessionService implements ISessionService { - private session$ = new BehaviorSubject(undefined); - private get sessionId() { - return this.session$.getValue(); - } - private appChangeSubscription$?: Subscription; - private curApp?: string; - private http!: HttpStart; - - /** - * Has the session already been stored (i.e. "sent to background")? - */ - private _isStored: boolean = false; - - /** - * Is this session a restored session (have these requests already been made, and we're just - * looking to re-use the previous search IDs)? - */ - private _isRestore: boolean = false; - - constructor( - initializerContext: PluginInitializerContext, - getStartServices: StartServicesAccessor - ) { - /* - Make sure that apps don't leave sessions open. - */ - getStartServices().then(([coreStart]) => { - this.http = coreStart.http; - - this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { - if (this.sessionId) { - const message = `Application '${this.curApp}' had an open session while navigating`; - if (initializerContext.env.mode.dev) { - // TODO: This setTimeout is necessary due to a race condition while navigating. - setTimeout(() => { - coreStart.fatalErrors.add(message); - }, 100); - } else { - // eslint-disable-next-line no-console - console.warn(message); - } - } - this.curApp = appName; - }); - }); - } - - public destroy() { - this.appChangeSubscription$?.unsubscribe(); - } - - public getSessionId() { - return this.sessionId; - } - - public getSession$() { - return this.session$.asObservable(); - } - - public isStored() { - return this._isStored; - } - - public isRestore() { - return this._isRestore; - } - - public start() { - this._isStored = false; - this._isRestore = false; - this.session$.next(uuid.v4()); - return this.sessionId!; - } - - public restore(sessionId: string) { - this._isStored = true; - this._isRestore = true; - this.session$.next(sessionId); - return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); - } - - public clear() { - this._isStored = false; - this._isRestore = false; - this.session$.next(undefined); - } - - public async save(name: string, url: string) { - const response = await this.http.post(`/internal/session`, { - body: JSON.stringify({ - name, - url, - sessionId: this.sessionId, - }), - }); - this._isStored = true; - return response; - } - - public get(sessionId: string) { - return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); - } - - public find(options: SearchSessionFindOptions) { - return this.http.post(`/internal/session`, { - body: JSON.stringify(options), - }); - } - - public update(sessionId: string, attributes: Partial) { - return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, { - body: JSON.stringify(attributes), - }); - } - - public delete(sessionId: string) { - return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`); - } -} diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index c08d9f4c7be3f..057b242c22f20 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,9 +21,10 @@ import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { ISearchGeneric, ISessionService, ISearchStartSearchSource } from '../../common/search'; +import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { ISessionsClient, ISessionService } from './session'; export { ISearchStartSearchSource }; @@ -39,10 +40,15 @@ export interface ISearchSetup { aggs: AggsSetup; usageCollector?: SearchUsageCollector; /** - * session management + * Current session management * {@link ISessionService} */ session: ISessionService; + /** + * Background search sessions SO CRUD + * {@link ISessionsClient} + */ + sessionsClient: ISessionsClient; /** * @internal */ @@ -73,10 +79,15 @@ export interface ISearchStart { */ searchSource: ISearchStartSearchSource; /** - * session management + * Current session management * {@link ISessionService} */ session: ISessionService; + /** + * Background search sessions SO CRUD + * {@link ISessionsClient} + */ + sessionsClient: ISessionsClient; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts index 74b03c4d867e4..e81272628c091 100644 --- a/src/plugins/data/server/saved_objects/background_session.ts +++ b/src/plugins/data/server/saved_objects/background_session.ts @@ -39,6 +39,12 @@ export const backgroundSessionMapping: SavedObjectsType = { status: { type: 'keyword', }, + appId: { + type: 'keyword', + }, + urlGeneratorId: { + type: 'keyword', + }, initialState: { type: 'object', enabled: false, diff --git a/src/plugins/data/server/search/routes/session.ts b/src/plugins/data/server/search/routes/session.ts index 93f07ecfb92ff..f7dfc776565e0 100644 --- a/src/plugins/data/server/search/routes/session.ts +++ b/src/plugins/data/server/search/routes/session.ts @@ -28,19 +28,31 @@ export function registerSessionRoutes(router: IRouter): void { body: schema.object({ sessionId: schema.string(), name: schema.string(), + appId: schema.string(), expires: schema.maybe(schema.string()), + urlGeneratorId: schema.string(), initialState: schema.maybe(schema.object({}, { unknowns: 'allow' })), restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, }, async (context, request, res) => { - const { sessionId, name, expires, initialState, restoreState } = request.body; + const { + sessionId, + name, + expires, + initialState, + restoreState, + appId, + urlGeneratorId, + } = request.body; try { const response = await context.search!.session.save(sessionId, { name, + appId, expires, + urlGeneratorId, initialState, restoreState, }); diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 1ceebae967d4c..5ff6d4b932487 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -33,6 +33,8 @@ describe('BackgroundSessionService', () => { type: BACKGROUND_SESSION_TYPE, attributes: { name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', idMapping: {}, }, references: [], @@ -121,6 +123,8 @@ describe('BackgroundSessionService', () => { const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const isStored = false; const name = 'my saved background search session'; + const appId = 'my_app_id'; + const urlGeneratorId = 'my_url_generator_id'; const created = new Date().toISOString(); const expires = new Date().toISOString(); @@ -133,7 +137,11 @@ describe('BackgroundSessionService', () => { expect(savedObjectsClient.update).not.toHaveBeenCalled(); - await service.save(sessionId, { name, created, expires }, { savedObjectsClient }); + await service.save( + sessionId, + { name, created, expires, appId, urlGeneratorId }, + { savedObjectsClient } + ); expect(savedObjectsClient.create).toHaveBeenCalledWith( BACKGROUND_SESSION_TYPE, @@ -145,6 +153,8 @@ describe('BackgroundSessionService', () => { restoreState: {}, status: BackgroundSessionStatus.IN_PROGRESS, idMapping: { [requestHash]: searchId }, + appId, + urlGeneratorId, }, { id: sessionId } ); @@ -215,6 +225,8 @@ describe('BackgroundSessionService', () => { type: BACKGROUND_SESSION_TYPE, attributes: { name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', idMapping: { [requestHash]: searchId }, }, references: [], diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index eca5f428b8555..b9a738413ede4 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -64,20 +64,34 @@ export class BackgroundSessionService { sessionId: string, { name, + appId, created = new Date().toISOString(), expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), status = BackgroundSessionStatus.IN_PROGRESS, + urlGeneratorId, initialState = {}, restoreState = {}, }: Partial, { savedObjectsClient }: BackgroundSessionDependencies ) => { if (!name) throw new Error('Name is required'); + if (!appId) throw new Error('AppId is required'); + if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); // Get the mapping of request hash/search ID for this session const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); const idMapping = Object.fromEntries(searchMap.entries()); - const attributes = { name, created, expires, status, initialState, restoreState, idMapping }; + const attributes = { + name, + created, + expires, + status, + initialState, + restoreState, + idMapping, + urlGeneratorId, + appId, + }; const session = await savedObjectsClient.create( BACKGROUND_SESSION_TYPE, attributes, diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 7059593c0c4e7..d0340c2cf4edd 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -23,7 +23,7 @@ import { debounceTime } from 'rxjs/operators'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; -import { getState, splitState } from './discover_state'; +import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; import { @@ -60,14 +60,14 @@ import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_patte import { addFatalError } from '../../../../kibana_legacy/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; -import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public'; +import { getQueryParams, removeQueryParam } from '../../../../kibana_utils/public'; import { DEFAULT_COLUMNS_SETTING, MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_ON_PAGE_LOAD_SETTING, } from '../../../common'; -import { resolveIndexPattern, loadIndexPattern } from '../helpers/resolve_index_pattern'; +import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern'; import { getTopNavLinks } from '../components/top_nav/get_top_nav_links'; import { updateSearchSource } from '../helpers/update_search_source'; import { calcFieldCounts } from '../helpers/calc_field_counts'; @@ -85,7 +85,7 @@ const { toastNotifications, uiSettings: config, trackUiMetric, -} = services; +} = getServices(); const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -204,12 +204,20 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise // used for restoring background session let isInitialSearch = true; + // search session requested a data refresh + subscriptions.add( + data.search.session.onRefresh$.subscribe(() => { + refetch$.next(); + }) + ); + const state = getState({ getStateDefaults, storeInSessionStorage: config.get('state:storeInSessionStorage'), history, toasts: core.notifications.toasts, }); + const { appStateContainer, startSync: startStateSync, @@ -280,6 +288,14 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise } }); + data.search.session.setSearchSessionInfoProvider( + createSearchSessionRestorationDataProvider({ + appStateContainer, + data, + getSavedSearchId: () => savedSearch.id, + }) + ); + $scope.setIndexPattern = async (id) => { const nextIndexPattern = await indexPatterns.get(id); if (nextIndexPattern) { diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 3c6ef1d3e4334..7de4ac27dd81f 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -20,15 +20,23 @@ import { isEqual } from 'lodash'; import { History } from 'history'; import { NotificationsStart } from 'kibana/public'; import { - createStateContainer, createKbnUrlStateStorage, - syncState, - ReduxLikeStateContainer, + createStateContainer, IKbnUrlStateStorage, + ReduxLikeStateContainer, + StateContainer, + syncState, withNotifyOnErrors, } from '../../../../kibana_utils/public'; -import { esFilters, Filter, Query } from '../../../../data/public'; +import { + DataPublicPluginStart, + esFilters, + Filter, + Query, + SearchSessionInfoProvider, +} from '../../../../data/public'; import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; +import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; export interface AppState { /** @@ -247,3 +255,47 @@ export function isEqualState(stateA: AppState, stateB: AppState) { const { filters: stateBFilters = [], ...stateBPartial } = stateB; return isEqual(stateAPartial, stateBPartial) && isEqualFilters(stateAFilters, stateBFilters); } + +export function createSearchSessionRestorationDataProvider(deps: { + appStateContainer: StateContainer; + data: DataPublicPluginStart; + getSavedSearchId: () => string | undefined; +}): SearchSessionInfoProvider { + return { + getName: async () => 'Discover', + getUrlGeneratorData: async () => { + return { + urlGeneratorId: DISCOVER_APP_URL_GENERATOR, + initialState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), + restoreState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + }; + }, + }; +} + +function createUrlGeneratorState({ + appStateContainer, + data, + getSavedSearchId, + forceAbsoluteTime, // TODO: not implemented +}: { + appStateContainer: StateContainer; + data: DataPublicPluginStart; + getSavedSearchId: () => string | undefined; + forceAbsoluteTime: boolean; +}): DiscoverUrlGeneratorState { + const appState = appStateContainer.get(); + return { + filters: data.query.filterManager.getFilters(), + indexPatternId: appState.index, + query: appState.query, + savedSearchId: getSavedSearchId(), + timeRange: data.query.timefilter.timefilter.getTime(), // TODO: handle relative time range + searchSessionId: data.search.session.getSessionId(), + columns: appState.columns, + sort: appState.sort, + savedQuery: appState.savedQuery, + interval: appState.interval, + useHash: false, + }; +} diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index 98b7625e63c72..95bff6b1fdc9c 100644 --- a/src/plugins/discover/public/url_generator.test.ts +++ b/src/plugins/discover/public/url_generator.test.ts @@ -221,6 +221,19 @@ describe('Discover url generator', () => { expect(url).toContain('__test__'); }); + test('can specify columns, interval, sort and savedQuery', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + columns: ['_source'], + interval: 'auto', + sort: [['timestamp, asc']], + savedQuery: '__savedQueryId__', + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/discover#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` + ); + }); + describe('useHash property', () => { describe('when default useHash is set to false', () => { test('when using default, sets index pattern ID in the generated URL', async () => { diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index df9b16a4627ec..6d86818910b11 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -52,7 +52,7 @@ export interface DiscoverUrlGeneratorState { refreshInterval?: RefreshInterval; /** - * Optionally apply filers. + * Optionally apply filters. */ filters?: Filter[]; @@ -72,6 +72,24 @@ export interface DiscoverUrlGeneratorState { * Background search session id */ searchSessionId?: string; + + /** + * Columns displayed in the table + */ + columns?: string[]; + + /** + * Used interval of the histogram + */ + interval?: string; + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][]; + /** + * id of the used saved query + */ + savedQuery?: string; } interface Params { @@ -88,20 +106,28 @@ export class DiscoverUrlGenerator public readonly id = DISCOVER_APP_URL_GENERATOR; public readonly createUrl = async ({ + useHash = this.params.useHash, filters, indexPatternId, query, refreshInterval, savedSearchId, timeRange, - useHash = this.params.useHash, searchSessionId, + columns, + savedQuery, + sort, + interval, }: DiscoverUrlGeneratorState): Promise => { const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; const appState: { query?: Query; filters?: Filter[]; index?: string; + columns?: string[]; + interval?: string; + sort?: string[][]; + savedQuery?: string; } = {}; const queryState: QueryState = {}; @@ -109,6 +135,10 @@ export class DiscoverUrlGenerator if (filters && filters.length) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); if (indexPatternId) appState.index = indexPatternId; + if (columns) appState.columns = columns; + if (savedQuery) appState.savedQuery = savedQuery; + if (sort) appState.sort = sort; + if (interval) appState.interval = interval; if (timeRange) queryState.time = timeRange; if (filters && filters.length) diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 023cb3d19b632..534ab0f331e87 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -34,6 +34,7 @@ import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { History } from 'history'; import { Href } from 'history'; +import { HttpSetup as HttpSetup_2 } from 'kibana/public'; import { I18nStart as I18nStart_2 } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { ISearchOptions } from 'src/plugins/data/public'; @@ -56,7 +57,9 @@ import { OverlayStart as OverlayStart_2 } from 'src/core/public'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import * as PropTypes from 'prop-types'; +import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; @@ -77,6 +80,7 @@ import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/ex import { ShallowPromise } from '@kbn/utility-types'; import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public'; import { Start as Start_2 } from 'src/plugins/inspector/public'; +import { StartServicesAccessor as StartServicesAccessor_2 } from 'kibana/public'; import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications'; import { ToastsSetup as ToastsSetup_2 } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index fa3206446f9fc..393a28c289e82 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -68,6 +68,7 @@ export class DataEnhancedPlugin React.createElement( createConnectedBackgroundSessionIndicator({ sessionService: plugins.data.search.session, + application: core.application, }) ) ), 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 f4d7422d1c7e2..20b55d9688edb 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,9 +9,10 @@ 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 { SearchTimeoutError } from 'src/plugins/data/public'; +import { ISessionService, SearchTimeoutError, SessionState } 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'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -43,11 +44,18 @@ function mockFetchImplementation(responses: any[]) { describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; + let sessionService: jest.Mocked; + let sessionState$: BehaviorSubject; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + sessionState$ = new BehaviorSubject(SessionState.None); const dataPluginMockStart = dataPluginMock.createStartContract(); + sessionService = { + ...(dataPluginMockStart.search.session as jest.Mocked), + state$: sessionState$, + }; fetchMock = jest.fn(); mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { @@ -87,7 +95,7 @@ describe('EnhancedSearchInterceptor', () => { http: mockCoreSetup.http, uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, - session: dataPluginMockStart.search.session, + session: sessionService, }); }); @@ -144,6 +152,7 @@ describe('EnhancedSearchInterceptor', () => { }, }, ]; + mockFetchImplementation(responses); const response = searchInterceptor.search({}, { pollInterval: 0 }); @@ -361,6 +370,54 @@ describe('EnhancedSearchInterceptor', () => { expect(fetchMock).toHaveBeenCalledTimes(2); expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); + + test('should NOT DELETE a running SAVED async search on abort', async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + const responses = [ + { + time: 10, + value: { + isPartial: true, + isRunning: true, + id: 1, + }, + }, + { + time: 300, + value: { + isPartial: false, + isRunning: false, + id: 1, + }, + }, + ]; + mockFetchImplementation(responses); + + const abortController = new AbortController(); + setTimeout(() => abortController.abort(), 250); + + const response = searchInterceptor.search( + {}, + { abortSignal: abortController.signal, pollInterval: 0, sessionId } + ); + response.subscribe({ next, error }); + + await timeTravel(10); + + expect(next).toHaveBeenCalled(); + expect(error).not.toHaveBeenCalled(); + + sessionState$.next(SessionState.BackgroundLoading); + + await timeTravel(240); + + expect(error).toHaveBeenCalled(); + expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); + }); }); describe('cancelPending', () => { @@ -395,4 +452,108 @@ describe('EnhancedSearchInterceptor', () => { expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1); }); }); + + describe('session', () => { + beforeEach(() => { + const responses = [ + { + time: 10, + value: { + isPartial: true, + isRunning: true, + id: 1, + }, + }, + { + time: 300, + value: { + isPartial: false, + isRunning: false, + id: 1, + }, + }, + ]; + + mockFetchImplementation(responses); + }); + + test('should track searches', async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response = searchInterceptor.search({}, { pollInterval: 0, sessionId }); + response.subscribe({ next, error }); + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(1); + expect(untrack).not.toBeCalled(); + await timeTravel(300); + expect(sessionService.trackSearch).toBeCalledTimes(1); + expect(untrack).toBeCalledTimes(1); + }); + + test('session service should be able to cancel search', async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response = searchInterceptor.search({}, { pollInterval: 0, sessionId }); + response.subscribe({ next, error }); + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(1); + + const abort = sessionService.trackSearch.mock.calls[0][0].abort; + expect(abort).toBeInstanceOf(Function); + + abort(); + + await timeTravel(10); + + expect(error).toHaveBeenCalled(); + expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + }); + + test("don't track non current session searches", async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response1 = searchInterceptor.search( + {}, + { pollInterval: 0, sessionId: 'something different' } + ); + response1.subscribe({ next, error }); + + const response2 = searchInterceptor.search({}, { pollInterval: 0, sessionId: undefined }); + response2.subscribe({ next, error }); + + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(0); + }); + + test("don't track if no current session", async () => { + sessionService.getSessionId.mockImplementation(() => undefined); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response1 = searchInterceptor.search( + {}, + { pollInterval: 0, sessionId: 'something different' } + ); + response1.subscribe({ next, error }); + + const response2 = searchInterceptor.search({}, { pollInterval: 0, sessionId: undefined }); + response2.subscribe({ next, error }); + + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(0); + }); + }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index 9aa35b460b1e8..0e87c093d2a8d 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -5,13 +5,14 @@ */ import { throwError, Subscription } from 'rxjs'; -import { tap, finalize, catchError } from 'rxjs/operators'; +import { tap, finalize, catchError, filter, take, skip } from 'rxjs/operators'; import { TimeoutErrorMode, SearchInterceptor, SearchInterceptorDeps, UI_SETTINGS, IKibanaSearchRequest, + SessionState, } from '../../../../../src/plugins/data/public'; import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common'; @@ -54,7 +55,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }; public search({ id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions = {}) { - const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({ + const { combinedSignal, timeoutSignal, cleanup, abort } = this.setupAbortSignal({ abortSignal: options.abortSignal, timeout: this.searchTimeout, }); @@ -63,16 +64,41 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { const search = () => this.runSearch({ id, ...request }, searchOptions); this.pendingCount$.next(this.pendingCount$.getValue() + 1); + const isCurrentSession = () => + !!options.sessionId && options.sessionId === this.deps.session.getSessionId(); + + const untrackSearch = isCurrentSession() && this.deps.session.trackSearch({ abort }); + + // track if this search's session will be send to background + // if yes, then we don't need to cancel this search when it is aborted + let isSavedToBackground = false; + const savedToBackgroundSub = + isCurrentSession() && + this.deps.session.state$ + .pipe( + skip(1), // ignore any state, we are only interested in transition x -> BackgroundLoading + filter((state) => isCurrentSession() && state === SessionState.BackgroundLoading), + take(1) + ) + .subscribe(() => { + isSavedToBackground = true; + }); return pollSearch(search, { ...options, abortSignal: combinedSignal }).pipe( tap((response) => (id = response.id)), catchError((e: AbortError) => { - if (id) this.deps.http.delete(`/internal/search/${strategy}/${id}`); + if (id && !isSavedToBackground) this.deps.http.delete(`/internal/search/${strategy}/${id}`); return throwError(this.handleSearchError(e, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); + if (untrackSearch && isCurrentSession()) { + untrackSearch(); + } + if (savedToBackgroundSub) { + savedToBackgroundSub.unsubscribe(); + } }) ); } diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx index 9cef76c62279c..c7195ea486e2f 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx @@ -7,24 +7,24 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { BackgroundSessionIndicator } from './background_session_indicator'; -import { BackgroundSessionViewState } from '../connected_background_session_indicator'; +import { SessionState } from '../../../../../../../src/plugins/data/public'; storiesOf('components/BackgroundSessionIndicator', module).add('default', () => ( <>

- +
- +
- +
- +
- +
)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx index 5b7ab2e4f9b1f..f401a460113c7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx @@ -8,8 +8,8 @@ import React, { ReactNode } from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BackgroundSessionIndicator } from './background_session_indicator'; -import { BackgroundSessionViewState } from '../connected_background_session_indicator'; import { IntlProvider } from 'react-intl'; +import { SessionState } from '../../../../../../../src/plugins/data/public'; function Container({ children }: { children?: ReactNode }) { return {children}; @@ -19,7 +19,7 @@ test('Loading state', async () => { const onCancel = jest.fn(); render( - + ); @@ -33,10 +33,7 @@ test('Completed state', async () => { const onSave = jest.fn(); render( - + ); @@ -50,10 +47,7 @@ test('Loading in the background state', async () => { const onCancel = jest.fn(); render( - + ); @@ -64,30 +58,26 @@ test('Loading in the background state', async () => { }); test('BackgroundCompleted state', async () => { - const onViewSession = jest.fn(); render( ); await userEvent.click(screen.getByLabelText('Results loaded in the background')); - await userEvent.click(screen.getByText('View background sessions')); - - expect(onViewSession).toBeCalled(); + expect(screen.getByRole('link', { name: 'View background sessions' }).getAttribute('href')).toBe( + '__link__' + ); }); test('Restored state', async () => { const onRefresh = jest.fn(); render( - + ); @@ -96,3 +86,17 @@ test('Restored state', async () => { expect(onRefresh).toBeCalled(); }); + +test('Canceled state', async () => { + const onRefresh = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Canceled')); + await userEvent.click(screen.getByText('Refresh')); + + expect(onRefresh).toBeCalled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx index b55bd6b655371..674ce392aa2d0 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx @@ -19,14 +19,15 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { BackgroundSessionViewState } from '../connected_background_session_indicator'; + import './background_session_indicator.scss'; +import { SessionState } from '../../../../../../../src/plugins/data/public/'; export interface BackgroundSessionIndicatorProps { - state: BackgroundSessionViewState; + state: SessionState; onContinueInBackground?: () => void; onCancel?: () => void; - onViewBackgroundSessions?: () => void; + viewBackgroundSessionsLink?: string; onSaveResults?: () => void; onRefresh?: () => void; } @@ -34,7 +35,11 @@ export interface BackgroundSessionIndicatorProps { type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {}, }: ActionButtonProps) => ( - + {}, + viewBackgroundSessionsLink = 'management', buttonProps = {}, }: ActionButtonProps) => ( - // TODO: make this a link - + {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {} }: ActionButtonP ); const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {} }: ActionButton ); const backgroundSessionIndicatorViewStateToProps: { - [state in BackgroundSessionViewState]: { - button: Pick & { tooltipText: string }; + [state in SessionState]: { + button: Pick & { + tooltipText: string; + }; popover: { text: string; primaryAction?: React.ComponentType; secondaryAction?: React.ComponentType; }; - }; + } | null; } = { - [BackgroundSessionViewState.Loading]: { + [SessionState.None]: null, + [SessionState.Loading]: { button: { color: 'subdued', iconType: 'clock', @@ -116,7 +139,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ContinueInBackgroundButton, }, }, - [BackgroundSessionViewState.Completed]: { + [SessionState.Completed]: { button: { color: 'subdued', iconType: 'checkInCircleFilled', @@ -141,7 +164,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.BackgroundLoading]: { + [SessionState.BackgroundLoading]: { button: { iconType: EuiLoadingSpinner, 'aria-label': i18n.translate( @@ -165,7 +188,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.BackgroundCompleted]: { + [SessionState.BackgroundCompleted]: { button: { color: 'success', iconType: 'checkInCircleFilled', @@ -192,7 +215,7 @@ const backgroundSessionIndicatorViewStateToProps: { primaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.Restored]: { + [SessionState.Restored]: { button: { color: 'warning', iconType: 'refresh', @@ -217,6 +240,25 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, + [SessionState.Canceled]: { + button: { + color: 'subdued', + iconType: 'refresh', + 'aria-label': i18n.translate('xpack.data.backgroundSessionIndicator.canceledIconAriaLabel', { + defaultMessage: 'Canceled', + }), + tooltipText: i18n.translate('xpack.data.backgroundSessionIndicator.canceledTooltipText', { + defaultMessage: 'Search was canceled', + }), + }, + popover: { + text: i18n.translate('xpack.data.backgroundSessionIndicator.canceledText', { + defaultMessage: 'Search was canceled', + }), + primaryAction: RefreshButton, + secondaryAction: ViewBackgroundSessionsButton, + }, + }, }; const VerticalDivider: React.FC = () => ( @@ -228,7 +270,9 @@ export const BackgroundSessionIndicator: React.FC setIsPopoverOpen((isOpen) => !isOpen); const closePopover = () => setIsPopoverOpen(false); - const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]; + if (!backgroundSessionIndicatorViewStateToProps[props.state]) return null; + + const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]!; return ( diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx index d97d10512783c..4c9fd50dc8c4c 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx @@ -9,13 +9,18 @@ import { render, waitFor } from '@testing-library/react'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator'; import { BehaviorSubject } from 'rxjs'; -import { ISessionService } from '../../../../../../../src/plugins/data/public'; +import { ISessionService, SessionState } from '../../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +const coreStart = coreMock.createStart(); const sessionService = dataPluginMock.createStartContract().search .session as jest.Mocked; test("shouldn't show indicator in case no active search session", async () => { - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService, + application: coreStart.application, + }); const { getByTestId, container } = render(); // make sure `backgroundSessionIndicator` isn't appearing after some time (lazy-loading) @@ -26,10 +31,11 @@ test("shouldn't show indicator in case no active search session", async () => { }); test('should show indicator in case there is an active search session', async () => { - const session$ = new BehaviorSubject('session_id'); - sessionService.getSession$.mockImplementation(() => session$); - sessionService.getSessionId.mockImplementation(() => session$.getValue()); - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); + const state$ = new BehaviorSubject(SessionState.Loading); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + }); const { getByTestId } = render(); await waitFor(() => getByTestId('backgroundSessionIndicator')); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx index d097a1aecb66a..1cc2fffcea8c5 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx @@ -5,28 +5,43 @@ */ import React from 'react'; +import { debounceTime } from 'rxjs/operators'; import useObservable from 'react-use/lib/useObservable'; -import { distinctUntilChanged, map } from 'rxjs/operators'; import { BackgroundSessionIndicator } from '../background_session_indicator'; import { ISessionService } from '../../../../../../../src/plugins/data/public/'; -import { BackgroundSessionViewState } from './background_session_view_state'; +import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; +import { ApplicationStart } from '../../../../../../../src/core/public'; export interface BackgroundSessionIndicatorDeps { sessionService: ISessionService; + application: ApplicationStart; } export const createConnectedBackgroundSessionIndicator = ({ sessionService, + application, }: BackgroundSessionIndicatorDeps): React.FC => { - const sessionId$ = sessionService.getSession$(); - const hasActiveSession$ = sessionId$.pipe( - map((sessionId) => !!sessionId), - distinctUntilChanged() - ); - return () => { - const isSession = useObservable(hasActiveSession$, !!sessionService.getSessionId()); - if (!isSession) return null; - return ; + const state = useObservable(sessionService.state$.pipe(debounceTime(500))); + if (!state) return null; + return ( + + { + sessionService.save(); + }} + onSaveResults={() => { + sessionService.save(); + }} + onRefresh={() => { + sessionService.refresh(); + }} + onCancel={() => { + sessionService.cancel(); + }} + /> + + ); }; }; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts index adbb6edbbfcf3..223a0537129df 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts @@ -8,4 +8,3 @@ export { BackgroundSessionIndicatorDeps, createConnectedBackgroundSessionIndicator, } from './connected_background_session_indicator'; -export { BackgroundSessionViewState } from './background_session_view_state'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 6b7cc8167ede6..92657df7f9bb5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -45,13 +45,13 @@ describe('alert actions', () => { updateTimelineIsLoading = jest.fn() as jest.Mocked; searchStrategyClient = { + ...dataPluginMock.createStartContract().search, aggs: {} as ISearchStart['aggs'], showError: jest.fn(), search: jest .fn() .mockImplementation(() => ({ toPromise: () => ({ data: mockTimelineDetails }) })), searchSource: {} as ISearchStart['searchSource'], - session: dataPluginMock.createStartContract().search.session, }; jest.spyOn(apolloClient, 'query').mockImplementation((obj) => { diff --git a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts index 17497c8326777..c9db2b1221545 100644 --- a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const queryBar = getService('queryBar'); const browser = getService('browser'); + const sendToBackground = getService('sendToBackground'); describe('dashboard with async search', () => { before(async function () { @@ -78,21 +79,53 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(panel1SessionId1).not.to.be(panel1SessionId2); }); - // NOTE: this test will be revised when session functionality is really working - it('Opens a dashboard with existing session', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); - const url = await browser.getCurrentUrl(); - const fakeSessionId = '__fake__'; - const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; - await browser.navigateTo(savedSessionURL); - await PageObjects.header.waitUntilLoadingHasFinished(); - const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - expect(session1).to.be(fakeSessionId); - await queryBar.clickQuerySubmitButton(); - await PageObjects.header.waitUntilLoadingHasFinished(); - const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - expect(session2).not.to.be(fakeSessionId); + describe('Send to background', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + }); + + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + const url = await browser.getCurrentUrl(); + const fakeSessionId = '__fake__'; + const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await sendToBackground.expectState('restored'); + await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session + + const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + expect(session1).to.be(fakeSessionId); + + await sendToBackground.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await sendToBackground.expectState('completed'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + expect(session2).not.to.be(fakeSessionId); + }); + + it('Saves and restores a session', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + await PageObjects.dashboard.waitForRenderComplete(); + await sendToBackground.expectState('completed'); + await sendToBackground.save(); + await sendToBackground.expectState('backgroundCompleted'); + const savedSessionId = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + + // load URL to restore a saved session + const url = await browser.getCurrentUrl(); + const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // Check that session is restored + await sendToBackground.expectState('restored'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); + expect(data.length).to.be(5); + }); }); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 70e92e88e60be..e3f83f08eb758 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -89,6 +89,7 @@ export default async function ({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--timelion.ui.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects + '--xpack.data_enhanced.search.sendToBackground.enabled=true', // enable WIP send to background UI ], }, uiSettings: { diff --git a/x-pack/test/functional/services/data/index.ts b/x-pack/test/functional/services/data/index.ts new file mode 100644 index 0000000000000..c2e3fcb41a7c9 --- /dev/null +++ b/x-pack/test/functional/services/data/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 { SendToBackgroundProvider } from './send_to_background'; diff --git a/x-pack/test/functional/services/data/send_to_background.ts b/x-pack/test/functional/services/data/send_to_background.ts new file mode 100644 index 0000000000000..f6a28c59b737d --- /dev/null +++ b/x-pack/test/functional/services/data/send_to_background.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +const SEND_TO_BACKGROUND_TEST_SUBJ = 'backgroundSessionIndicator'; +const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'backgroundSessionIndicatorPopoverContainer'; + +type SessionStateType = + | 'none' + | 'loading' + | 'completed' + | 'backgroundLoading' + | 'backgroundCompleted' + | 'restored' + | 'canceled'; + +export function SendToBackgroundProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); + + return new (class SendToBackgroundService { + public async find(): Promise { + return testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ); + } + + public async exists(): Promise { + return testSubjects.exists(SEND_TO_BACKGROUND_TEST_SUBJ); + } + + public async expectState(state: SessionStateType) { + return retry.waitFor(`sendToBackground indicator to get into state = ${state}`, async () => { + const currentState = await ( + await testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ) + ).getAttribute('data-state'); + return currentState === state; + }); + } + + public async viewBackgroundSessions() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorViewBackgroundSessionsLink'); + } + + public async save() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorSaveBtn'); + await this.ensurePopoverClosed(); + } + + public async cancel() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorCancelBtn'); + await this.ensurePopoverClosed(); + } + + public async refresh() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorRefreshBtn'); + await this.ensurePopoverClosed(); + } + + private async ensurePopoverOpened() { + const isAlreadyOpen = await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + if (isAlreadyOpen) return; + return retry.waitFor(`sendToBackground popover opened`, async () => { + await testSubjects.click(SEND_TO_BACKGROUND_TEST_SUBJ); + return await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + }); + } + + private async ensurePopoverClosed() { + const isAlreadyClosed = !(await testSubjects.exists( + SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ + )); + if (isAlreadyClosed) return; + return retry.waitFor(`sendToBackground popover closed`, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + return !(await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ)); + }); + } + })(); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 1aa6216236827..d6d921d5bce17 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -56,6 +56,7 @@ import { DashboardDrilldownsManageProvider, DashboardPanelTimeRangeProvider, } from './dashboard'; +import { SendToBackgroundProvider } from './data'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -103,4 +104,5 @@ export const services = { dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, dashboardDrilldownsManage: DashboardDrilldownsManageProvider, dashboardPanelTimeRange: DashboardPanelTimeRangeProvider, + sendToBackground: SendToBackgroundProvider, }; From 4a7071ea904fc0a1f9d170c24911cdd4f29fa734 Mon Sep 17 00:00:00 2001 From: Bill McConaghy Date: Tue, 1 Dec 2020 08:03:57 -0500 Subject: [PATCH 013/107] adding documentation of use of NODE_EXTRA_CA_CERTS env var (#84578) --- docs/user/alerting/defining-alerts.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 05d022d039b23..667038739d45f 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -89,6 +89,8 @@ Here's a list of the available global configuration options and an explanation o * `xpack.actions.proxyRejectUnauthorizedCertificates`: Set to `false` to bypass certificate validation for proxy, if using a proxy for actions. * `xpack.actions.rejectUnauthorized`: Set to `false` to bypass certificate validation for actions. +*NOTE:* As an alternative to both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, the OS level environment variable `NODE_EXTRA_CA_CERTS` can be set to point to a file that contains the root CA(s) needed for certificates to be trusted. + [float] === Managing alerts From 0ba7d9ca1a7afeb2500a398a1eb1b166add1ec9c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 1 Dec 2020 08:46:57 -0500 Subject: [PATCH 014/107] [Fleet] Update agent details page (#84434) --- .../fleet/constants/page_paths.ts | 4 +- .../components/actions_menu.tsx | 2 +- .../components/agent_details.tsx | 147 -------------- .../agent_details_integrations.tsx | 159 +++++++++++++++ .../agent_details/agent_details_overview.tsx | 185 ++++++++++++++++++ .../components/agent_details/index.tsx | 47 +++++ .../agent_details/input_type_utils.ts | 43 ++++ .../components/agent_logs/constants.tsx | 4 + .../agents/agent_details_page/index.tsx | 39 ++-- .../agents/components/agent_health.tsx | 71 +++---- .../agent_policy_package_badges.tsx | 73 ++++--- .../public/applications/fleet/types/index.ts | 1 + .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 14 files changed, 530 insertions(+), 257 deletions(-) delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index ecd4227a54b65..2fce7f8f5e825 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -78,8 +78,8 @@ export const pagePathGetters: { `/policies/${policyId}/edit-integration/${packagePolicyId}`, fleet: () => '/fleet', fleet_agent_list: ({ kuery }) => `/fleet/agents${kuery ? `?kuery=${kuery}` : ''}`, - fleet_agent_details: ({ agentId, tabId }) => - `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, + fleet_agent_details: ({ agentId, tabId, logQuery }) => + `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}${logQuery ? `?_q=${logQuery}` : ''}`, fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', data_streams: () => '/data-streams', }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx index 487eac6779dd5..2b1eb8e1ce984 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx @@ -73,7 +73,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ )} = memo(({ agent, agentPolicy }) => { - const { getHref } = useLink(); - const kibanaVersion = useKibanaVersion(); - return ( - - {[ - { - title: i18n.translate('xpack.fleet.agentDetails.hostNameLabel', { - defaultMessage: 'Host name', - }), - description: - typeof agent.local_metadata.host === 'object' && - typeof agent.local_metadata.host.hostname === 'string' - ? agent.local_metadata.host.hostname - : '-', - }, - { - title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', { - defaultMessage: 'Agent ID', - }), - description: agent.id, - }, - { - title: i18n.translate('xpack.fleet.agentDetails.statusLabel', { - defaultMessage: 'Status', - }), - description: , - }, - { - title: i18n.translate('xpack.fleet.agentDetails.agentPolicyLabel', { - defaultMessage: 'Agent policy', - }), - description: agentPolicy ? ( - - {agentPolicy.name || agent.policy_id} - - ) : ( - agent.policy_id || '-' - ), - }, - { - title: i18n.translate('xpack.fleet.agentDetails.versionLabel', { - defaultMessage: 'Agent version', - }), - description: - typeof agent.local_metadata.elastic === 'object' && - typeof agent.local_metadata.elastic.agent === 'object' && - typeof agent.local_metadata.elastic.agent.version === 'string' ? ( - - - {agent.local_metadata.elastic.agent.version} - - {isAgentUpgradeable(agent, kibanaVersion) ? ( - - - -   - - - - ) : null} - - ) : ( - '-' - ), - }, - { - title: i18n.translate('xpack.fleet.agentDetails.releaseLabel', { - defaultMessage: 'Agent release', - }), - description: - typeof agent.local_metadata.elastic === 'object' && - typeof agent.local_metadata.elastic.agent === 'object' && - typeof agent.local_metadata.elastic.agent.snapshot === 'boolean' - ? agent.local_metadata.elastic.agent.snapshot === true - ? 'snapshot' - : 'stable' - : '-', - }, - { - title: i18n.translate('xpack.fleet.agentDetails.logLevel', { - defaultMessage: 'Log level', - }), - description: - typeof agent.local_metadata.elastic === 'object' && - typeof agent.local_metadata.elastic.agent === 'object' && - typeof agent.local_metadata.elastic.agent.log_level === 'string' - ? agent.local_metadata.elastic.agent.log_level - : '-', - }, - { - title: i18n.translate('xpack.fleet.agentDetails.platformLabel', { - defaultMessage: 'Platform', - }), - description: - typeof agent.local_metadata.os === 'object' && - typeof agent.local_metadata.os.platform === 'string' - ? agent.local_metadata.os.platform - : '-', - }, - ].map(({ title, description }) => { - return ( - - - {title} - - - {description} - - - ); - })} - - ); -}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx new file mode 100644 index 0000000000000..0cad0b4d487d0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -0,0 +1,159 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiAccordion, + EuiTitle, + EuiPanel, + EuiButtonIcon, + EuiBasicTable, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { Agent, AgentPolicy, PackagePolicy, PackagePolicyInput } from '../../../../../types'; +import { useLink } from '../../../../../hooks'; +import { PackageIcon } from '../../../../../components'; +import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; + +const StyledEuiAccordion = styled(EuiAccordion)` + .ingest-integration-title-button { + padding: ${(props) => props.theme.eui.paddingSizes.m} + ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; + +const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ + id, + title, + children, +}) => { + return ( + + + {children} + + + ); +}; + +export const AgentDetailsIntegration: React.FunctionComponent<{ + agent: Agent; + agentPolicy: AgentPolicy; + packagePolicy: PackagePolicy; +}> = memo(({ agent, agentPolicy, packagePolicy }) => { + const { getHref } = useLink(); + + const inputs = useMemo(() => { + return packagePolicy.inputs.filter((input) => input.enabled); + }, [packagePolicy.inputs]); + + const columns = [ + { + field: 'type', + width: '100%', + name: i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeLabel', { + defaultMessage: 'Input', + }), + render: (inputType: string) => { + return displayInputType(inputType); + }, + }, + { + name: i18n.translate('xpack.fleet.agentDetailsIntegrations.actionsLabel', { + defaultMessage: 'Actions', + }), + field: 'type', + width: 'auto', + render: (inputType: string) => { + return ( + + ); + }, + }, + ]; + + return ( + +

+ + + {packagePolicy.package ? ( + + ) : ( + + )} + + + + {packagePolicy.name} + + + +

+ + } + > + tableLayout="auto" items={inputs} columns={columns} /> +
+ ); +}); + +export const AgentDetailsIntegrationsSection: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; +}> = memo(({ agent, agentPolicy }) => { + if (!agentPolicy || !agentPolicy.package_policies) { + return null; + } + + return ( + + {(agentPolicy.package_policies as PackagePolicy[]).map((packagePolicy) => { + return ( + + + + ); + })} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx new file mode 100644 index 0000000000000..a19f6658ef93f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -0,0 +1,185 @@ +/* + * 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, { memo } from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Agent, AgentPolicy } from '../../../../../types'; +import { useKibanaVersion, useLink } from '../../../../../hooks'; +import { isAgentUpgradeable } from '../../../../../services'; +import { AgentPolicyPackageBadges } from '../../../components/agent_policy_package_badges'; +import { LinkAndRevision } from '../../../../../components'; + +export const AgentDetailsOverviewSection: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; +}> = memo(({ agent, agentPolicy }) => { + const { getHref } = useLink(); + const kibanaVersion = useKibanaVersion(); + return ( + + + {[ + { + title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', { + defaultMessage: 'Agent ID', + }), + description: agent.id, + }, + { + title: i18n.translate('xpack.fleet.agentDetails.agentPolicyLabel', { + defaultMessage: 'Agent policy', + }), + description: agentPolicy ? ( + + {agentPolicy.name || agentPolicy.id} + + ) : ( + agent.policy_id || '-' + ), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.versionLabel', { + defaultMessage: 'Agent version', + }), + description: + typeof agent.local_metadata?.elastic?.agent?.version === 'string' ? ( + + + {agent.local_metadata.elastic.agent.version} + + {isAgentUpgradeable(agent, kibanaVersion) ? ( + + + +   + + + + ) : null} + + ) : ( + '-' + ), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.enrollmentTokenLabel', { + defaultMessage: 'Enrollment token', + }), + description: '-', // Fixme when we have the enrollment tokenhttps://github.com/elastic/kibana/issues/61269 + }, + { + title: i18n.translate('xpack.fleet.agentDetails.integrationsLabel', { + defaultMessage: 'Integrations', + }), + description: agent.policy_id ? ( + + ) : null, + }, + { + title: i18n.translate('xpack.fleet.agentDetails.hostNameLabel', { + defaultMessage: 'Host name', + }), + description: + typeof agent.local_metadata?.host?.hostname === 'string' + ? agent.local_metadata.host.hostname + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.logLevel', { + defaultMessage: 'Logging level', + }), + description: + typeof agent.local_metadata?.elastic?.agent?.log_level === 'string' + ? agent.local_metadata.elastic.agent.log_level + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.releaseLabel', { + defaultMessage: 'Agent release', + }), + description: + typeof agent.local_metadata?.elastic?.agent?.snapshot === 'boolean' + ? agent.local_metadata.elastic.agent.snapshot === true + ? 'snapshot' + : 'stable' + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.platformLabel', { + defaultMessage: 'Platform', + }), + description: + typeof agent.local_metadata?.os?.platform === 'string' + ? agent.local_metadata.os.platform + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.monitorLogsLabel', { + defaultMessage: 'Monitor logs', + }), + description: agentPolicy?.monitoring_enabled?.includes('logs') ? ( + + ) : ( + + ), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.monitorMetricsLabel', { + defaultMessage: 'Monitor metrics', + }), + description: agentPolicy?.monitoring_enabled?.includes('metrics') ? ( + + ) : ( + + ), + }, + ].map(({ title, description }) => { + return ( + + + + {title} + + + {description} + + + + ); + })} + + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx new file mode 100644 index 0000000000000..0b83fb4cc64e1 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Agent, AgentPolicy } from '../../../../../types'; +import { AgentDetailsOverviewSection } from './agent_details_overview'; +import { AgentDetailsIntegrationsSection } from './agent_details_integrations'; + +export const AgentDetailsContent: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; +}> = memo(({ agent, agentPolicy }) => { + return ( + <> + + + +

+ +

+
+ + +
+ + +

+ +

+
+ + +
+
+ + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts new file mode 100644 index 0000000000000..62b7a294e1750 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { + STATE_DATASET_FIELD, + AGENT_DATASET_FILEBEAT, + AGENT_DATASET_METRICBEAT, +} from '../agent_logs/constants'; + +export function displayInputType(inputType: string): string { + if (inputType === 'logfile') { + return i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeLogText', { + defaultMessage: 'Logs', + }); + } + if (inputType === 'endpoint') { + return i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeEndpointText', { + defaultMessage: 'Endpoint', + }); + } + if (inputType.match(/\/metrics$/)) { + return i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeMetricsText', { + defaultMessage: 'Metrics', + }); + } + + return inputType; +} + +export function getLogsQueryByInputType(inputType: string) { + if (inputType === 'logfile') { + return `(${STATE_DATASET_FIELD}:!(${AGENT_DATASET_FILEBEAT}))`; + } + if (inputType.match(/\/metrics$/)) { + return `(${STATE_DATASET_FIELD}:!(${AGENT_DATASET_METRICBEAT}))`; + } + + return ''; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx index 89fe1a916605d..4ee1618a38584 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx @@ -7,6 +7,8 @@ import { AgentLogsState } from './agent_logs'; export const AGENT_LOG_INDEX_PATTERN = 'logs-elastic_agent-*,logs-elastic_agent.*-*'; export const AGENT_DATASET = 'elastic_agent'; +export const AGENT_DATASET_FILEBEAT = 'elastic_agent.filebeat'; +export const AGENT_DATASET_METRICBEAT = 'elastic_agent.metricbeat'; export const AGENT_DATASET_PATTERN = 'elastic_agent.*'; export const AGENT_ID_FIELD = { name: 'elastic_agent.id', @@ -34,6 +36,8 @@ export const DEFAULT_LOGS_STATE: AgentLogsState = { query: '', }; +export const STATE_DATASET_FIELD = 'datasets'; + export const AGENT_LOG_LEVELS = { ERROR: 'error', WARNING: 'warning', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index f3714bbb53223..34893dccd93a4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -5,7 +5,6 @@ */ import React, { useMemo, useCallback } from 'react'; import { useRouteMatch, Switch, Route, useLocation } from 'react-router-dom'; -import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +16,7 @@ import { EuiDescriptionListDescription, } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { Agent, AgentPolicy, AgentDetailsReassignPolicyAction } from '../../../types'; @@ -38,12 +37,6 @@ import { AgentLogs, AgentDetailsActionMenu, AgentDetailsContent } from './compon import { useIntraAppState } from '../../../hooks/use_intra_app_state'; import { isAgentUpgradeable } from '../../../services'; -const Divider = styled.div` - width: 0; - height: 100%; - border-left: ${(props) => props.theme.eui.euiBorderThin}; -`; - export const AgentDetailsPage: React.FunctionComponent = () => { const { params: { agentId, tabId = '' }, @@ -78,6 +71,8 @@ export const AgentDetailsPage: React.FunctionComponent = () => { } }, [routeState, navigateToApp]); + const host = agentData?.item?.local_metadata?.host; + const headerLeftContent = useMemo( () => ( @@ -99,9 +94,8 @@ export const AgentDetailsPage: React.FunctionComponent = () => {

{isLoading && isInitialRequest ? ( - ) : typeof agentData?.item?.local_metadata?.host === 'object' && - typeof agentData?.item?.local_metadata?.host?.hostname === 'string' ? ( - agentData.item.local_metadata.host.hostname + ) : typeof host === 'object' && typeof host?.hostname === 'string' ? ( + host.hostname ) : ( { ), - [agentData?.item?.local_metadata?.host, agentId, getHref, isInitialRequest, isLoading] + [host, agentId, getHref, isInitialRequest, isLoading] ); const headerRightContent = useMemo( () => agentData && agentData.item ? ( - + {[ { label: i18n.translate('xpack.fleet.agentDetails.statusLabel', { @@ -130,7 +124,16 @@ export const AgentDetailsPage: React.FunctionComponent = () => { }), content: , }, - { isDivider: true }, + { + label: i18n.translate('xpack.fleet.agentDetails.lastActivityLabel', { + defaultMessage: 'Last activity', + }), + content: agentData.item.last_checkin ? ( + + ) : ( + '-' + ), + }, { label: i18n.translate('xpack.fleet.agentDetails.policyLabel', { defaultMessage: 'Policy', @@ -148,7 +151,6 @@ export const AgentDetailsPage: React.FunctionComponent = () => { agentData.item.policy_id || '-' ), }, - { isDivider: true }, { label: i18n.translate('xpack.fleet.agentDetails.agentVersionLabel', { defaultMessage: 'Agent version', @@ -187,7 +189,6 @@ export const AgentDetailsPage: React.FunctionComponent = () => { '-' ), }, - { isDivider: true }, { content: ( { }, ].map((item, index) => ( - {item.isDivider ?? false ? ( - - ) : item.label ? ( - + {item.label ? ( + {item.label} {item.content} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index 45017ac8532da..40d91f13db659 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; -import { EuiHealth, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { Agent } from '../../../types'; interface Props { @@ -13,79 +13,52 @@ interface Props { } const Status = { - Online: ( - - - + Healthy: ( + + + ), Offline: ( - + - + ), Inactive: ( - - - - ), - Warning: ( - - - - ), - Error: ( - - - - ), - Degraded: ( - - - - ), - Enrolling: ( - - - + + + ), - Unenrolling: ( - + Unhealthy: ( + - + ), - Upgrading: ( - + Updating: ( + - + ), }; function getStatusComponent(agent: Agent): React.ReactElement { switch (agent.status) { + case 'warning': case 'error': - return Status.Error; case 'degraded': - return Status.Degraded; + return Status.Unhealthy; case 'inactive': return Status.Inactive; case 'offline': return Status.Offline; - case 'warning': - return Status.Warning; case 'unenrolling': - return Status.Unenrolling; case 'enrolling': - return Status.Enrolling; case 'updating': - return Status.Upgrading; + return Status.Updating; default: - return Status.Online; + return Status.Healthy; } } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx index 08835cc872b82..ff8e4868b1fdf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx @@ -3,53 +3,74 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import { PackagePolicy } from '../../../types'; +import { PackagePolicy, PackagePolicyPackage } from '../../../types'; import { useGetOneAgentPolicy } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; interface Props { agentPolicyId: string; + hideTitle?: boolean; } -export const AgentPolicyPackageBadges: React.FunctionComponent = ({ agentPolicyId }) => { +export const AgentPolicyPackageBadges: React.FunctionComponent = ({ + agentPolicyId, + hideTitle, +}) => { const agentPolicyRequest = useGetOneAgentPolicy(agentPolicyId); const agentPolicy = agentPolicyRequest.data ? agentPolicyRequest.data.item : null; - if (!agentPolicy) { + const packages = useMemo(() => { + if (!agentPolicy) { + return; + } + + const uniquePackages = new Map(); + + (agentPolicy.package_policies as PackagePolicy[]).forEach(({ package: pkg }) => { + if (!pkg) { + return; + } + + if (!uniquePackages.has(pkg.name) || uniquePackages.get(pkg.name)!.version < pkg.version) { + uniquePackages.set(pkg.name, pkg); + } + }); + + return [...uniquePackages.values()]; + }, [agentPolicy]); + + if (!agentPolicy || !packages) { return null; } + return ( <> - - {agentPolicy.package_policies.length}, - }} - /> - - - {(agentPolicy.package_policies as PackagePolicy[]).map((packagePolicy, idx) => { - if (!packagePolicy.package) { - return null; - } + {!hideTitle && ( + <> + + {packages.length}, + }} + /> + + + + )} + {packages.map((pkg, idx) => { return ( - + - {packagePolicy.package.title} + {pkg.title} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index ded1447954aff..dd80c1ad77b85 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -22,6 +22,7 @@ export { NewPackagePolicyInputStream, PackagePolicyConfigRecord, PackagePolicyConfigRecordEntry, + PackagePolicyPackage, Output, DataStream, // API schema - misc setup, status diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5af905fcfc66b..490bf56d76062 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7175,16 +7175,10 @@ "xpack.fleet.agentEnrollment.stepRunAgentDescription": "エージェントのディレクトリから、このコマンドを実行し、Elasticエージェントを、インストール、登録、起動します。このコマンドを再利用すると、複数のホストでエージェントを設定できます。管理者権限が必要です。", "xpack.fleet.agentEnrollment.stepRunAgentTitle": "エージェントの起動", "xpack.fleet.agentHealth.checkInTooltipText": "前回のチェックイン {lastCheckIn}", - "xpack.fleet.agentHealth.degradedStatusText": "劣化", - "xpack.fleet.agentHealth.enrollingStatusText": "登録中", - "xpack.fleet.agentHealth.errorStatusText": "エラー", "xpack.fleet.agentHealth.inactiveStatusText": "非アクティブ", "xpack.fleet.agentHealth.noCheckInTooltipText": "チェックインしない", "xpack.fleet.agentHealth.offlineStatusText": "オフライン", - "xpack.fleet.agentHealth.onlineStatusText": "オンライン", - "xpack.fleet.agentHealth.unenrollingStatusText": "登録解除中", "xpack.fleet.agentHealth.updatingStatusText": "更新中", - "xpack.fleet.agentHealth.warningStatusText": "エラー", "xpack.fleet.agentList.actionsColumnTitle": "アクション", "xpack.fleet.agentList.addButton": "エージェントの追加", "xpack.fleet.agentList.agentUpgradeLabel": "アップグレードが利用可能です", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 282a0c9067587..15699c3f47dd5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7181,16 +7181,10 @@ "xpack.fleet.agentEnrollment.stepRunAgentDescription": "从代理目录运行此命令,以安装、注册并启动 Elastic 代理。您可以重复使用此命令在多个主机上设置代理。需要管理员权限。", "xpack.fleet.agentEnrollment.stepRunAgentTitle": "启动代理", "xpack.fleet.agentHealth.checkInTooltipText": "上次签入时间 {lastCheckIn}", - "xpack.fleet.agentHealth.degradedStatusText": "已降级", - "xpack.fleet.agentHealth.enrollingStatusText": "正在注册", - "xpack.fleet.agentHealth.errorStatusText": "错误", "xpack.fleet.agentHealth.inactiveStatusText": "非活动", "xpack.fleet.agentHealth.noCheckInTooltipText": "未签入", "xpack.fleet.agentHealth.offlineStatusText": "脱机", - "xpack.fleet.agentHealth.onlineStatusText": "联机", - "xpack.fleet.agentHealth.unenrollingStatusText": "正在取消注册", "xpack.fleet.agentHealth.updatingStatusText": "正在更新", - "xpack.fleet.agentHealth.warningStatusText": "错误", "xpack.fleet.agentList.actionsColumnTitle": "操作", "xpack.fleet.agentList.addButton": "添加代理", "xpack.fleet.agentList.agentUpgradeLabel": "升级可用", From c0d7ce7de12e94bbfb2ee3392c6f67e853ad34df Mon Sep 17 00:00:00 2001 From: Georgii Gorbachev Date: Tue, 1 Dec 2020 14:50:13 +0100 Subject: [PATCH 015/107] [Security Solution][Detections] Fix grammatical error in validation message for threshold field in "Create new rule" -> "Define rule" (#84490) Just a simple tweak of the default translation. --- .../detections/components/rules/step_define_rule/schema.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 5203a630b72ae..de2d390ee6784 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -221,7 +221,7 @@ export const schema: FormSchema = { message: i18n.translate( 'xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage', { - defaultMessage: 'Value must be greater than or equal one.', + defaultMessage: 'Value must be greater than or equal to one.', } ), allowEquality: true, From 80e88cec4de67bbb9bae7182744f34100e7ab8dc Mon Sep 17 00:00:00 2001 From: Georgii Gorbachev Date: Tue, 1 Dec 2020 14:57:53 +0100 Subject: [PATCH 016/107] [Security Solution][Detections] Support arrays in event fields for Severity/Risk overrides (#83723) This PR changes the behavior of severity and risk score overrides in two ways: - adds support for arrays in the mapped event fields (so a rule can be triggered by an event where e.g. `event.custom_severity` has a value like `[45, 70, 90]`) - makes the logic of overrides more flexible, resilient to the incoming values (filters out junk, extracts meaningful values, does its best to find a value that would fit the mapping) --- .../signals/__mocks__/es_results.ts | 25 +- .../build_risk_score_from_mapping.test.ts | 213 +++++++++++++++++- .../mappings/build_risk_score_from_mapping.ts | 73 ++++-- .../build_severity_from_mapping.test.ts | 184 +++++++++++---- .../mappings/build_severity_from_mapping.ts | 113 +++++++--- .../tests/generating_signals.ts | 153 +++++++++++++ .../signals/severity_risk_overrides/data.json | 55 +++++ .../severity_risk_overrides/mappings.json | 26 +++ 8 files changed, 743 insertions(+), 99 deletions(-) create mode 100644 x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json create mode 100644 x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 92e6b9562d970..0a38bdc790b41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { set } from '@elastic/safer-lodash-set'; import { SignalSourceHit, SignalSearchResponse, @@ -189,9 +190,25 @@ export const sampleDocNoSortId = ( sort: [], }); -export const sampleDocSeverity = ( - severity?: Array | string | number | null -): SignalSourceHit => ({ +export const sampleDocSeverity = (severity?: unknown, fieldName?: string): SignalSourceHit => { + const doc = { + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: sampleIdGuid, + _source: { + someKey: 'someValue', + '@timestamp': '2020-04-20T21:27:45+0000', + }, + sort: [], + }; + + set(doc._source, fieldName ?? 'event.severity', severity); + return doc; +}; + +export const sampleDocRiskScore = (riskScore?: unknown): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -201,7 +218,7 @@ export const sampleDocSeverity = ( someKey: 'someValue', '@timestamp': '2020-04-20T21:27:45+0000', event: { - severity: severity ?? 100, + risk: riskScore, }, }, sort: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts index ff50c2634dfd1..9395085dd6e99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -4,23 +4,218 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId } from '../__mocks__/es_results'; -import { buildRiskScoreFromMapping } from './build_risk_score_from_mapping'; +import { + RiskScore, + RiskScoreMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { sampleDocRiskScore } from '../__mocks__/es_results'; +import { + buildRiskScoreFromMapping, + BuildRiskScoreFromMappingReturn, +} from './build_risk_score_from_mapping'; describe('buildRiskScoreFromMapping', () => { beforeEach(() => { jest.clearAllMocks(); }); - test('risk score defaults to provided if mapping is incomplete', () => { - const riskScore = buildRiskScoreFromMapping({ - eventSource: sampleDocNoSortId()._source, - riskScore: 57, - riskScoreMapping: undefined, + describe('base cases: when mapping is undefined', () => { + test('returns the provided default score', () => { + testIt({ + fieldValue: 42, + scoreDefault: 57, + scoreMapping: undefined, + expected: scoreOf(57), + }); }); + }); + + describe('base cases: when mapping to a field of type number', () => { + test(`returns that number if it's integer and within the range [0;100]`, () => { + testIt({ + fieldValue: 42, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(42), + }); + }); + + test(`returns that number if it's float and within the range [0;100]`, () => { + testIt({ + fieldValue: 3.14, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); + + test(`returns default score if the number is < 0`, () => { + testIt({ + fieldValue: -0.0000000000001, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + + test(`returns default score if the number is > 100`, () => { + testIt({ + fieldValue: 100.0000000000001, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + }); + + describe('base cases: when mapping to a field of type string', () => { + test(`returns the number casted from string if it's integer and within the range [0;100]`, () => { + testIt({ + fieldValue: '42', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(42), + }); + }); + + test(`returns the number casted from string if it's float and within the range [0;100]`, () => { + testIt({ + fieldValue: '3.14', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); + + test(`returns default score if the "number" is < 0`, () => { + testIt({ + fieldValue: '-1', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + + test(`returns default score if the "number" is > 100`, () => { + testIt({ + fieldValue: '101', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + }); - expect(riskScore).toEqual({ riskScore: 57, riskScoreMeta: {} }); + describe('base cases: when mapping to an array of numbers or strings', () => { + test(`returns that number if it's a single element and it's within the range [0;100]`, () => { + testIt({ + fieldValue: [3.14], + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); + + test(`returns the max number of those that are within the range [0;100]`, () => { + testIt({ + fieldValue: [42, -42, 17, 87, 87.5, '86.5', 110, 66], + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(87.5), + }); + }); + + test(`supports casting strings to numbers`, () => { + testIt({ + fieldValue: [-1, 1, '3', '1.5', '3.14', 2], + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); }); - // TODO: Enhance... + describe('edge cases: when mapping to a single junk value', () => { + describe('ignores it and returns the default score', () => { + const cases = [ + undefined, + null, + NaN, + Infinity, + -Infinity, + Number.MAX_VALUE, + -Number.MAX_VALUE, + -Number.MIN_VALUE, + 'string', + [], + {}, + new Date(), + ]; + + test.each(cases)('%p', (value) => { + testIt({ + fieldValue: value, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + }); + }); + + describe('edge cases: when mapping to an array of junk values', () => { + describe('ignores junk, extracts valid numbers and returns the max number within the range [0;100]', () => { + type Case = [unknown[], number]; + const cases: Case[] = [ + [[undefined, null, 1.5, 1, -Infinity], 1.5], + [['42', NaN, '44', '43', 42, {}], 44], + [[Infinity, '101', 100, 99, Number.MIN_VALUE], 100], + [[Number.MIN_VALUE, -0], Number.MIN_VALUE], + ]; + + test.each(cases)('%p', (value, expectedScore) => { + testIt({ + fieldValue: value, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(expectedScore), + }); + }); + }); + }); }); + +interface TestCase { + fieldValue: unknown; + scoreDefault: RiskScore; + scoreMapping: RiskScoreMappingOrUndefined; + expected: BuildRiskScoreFromMappingReturn; +} + +function testIt({ fieldValue, scoreDefault, scoreMapping, expected }: TestCase) { + const result = buildRiskScoreFromMapping({ + eventSource: sampleDocRiskScore(fieldValue)._source, + riskScore: scoreDefault, + riskScoreMapping: scoreMapping, + }); + + expect(result).toEqual(expected); +} + +function mappingToSingleField() { + return [{ field: 'event.risk', operator: 'equals' as const, value: '', risk_score: undefined }]; +} + +function scoreOf(value: number) { + return { + riskScore: value, + riskScoreMeta: {}, + }; +} + +function overriddenScoreOf(value: number) { + return { + riskScore: value, + riskScoreMeta: { riskScoreOverridden: true }, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts index c358339e66cd9..cb3fcba102350 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -11,35 +11,78 @@ import { } from '../../../../../common/detection_engine/schemas/common/schemas'; import { SignalSource } from '../types'; -interface BuildRiskScoreFromMappingProps { +export interface BuildRiskScoreFromMappingProps { eventSource: SignalSource; riskScore: RiskScore; riskScoreMapping: RiskScoreMappingOrUndefined; } -interface BuildRiskScoreFromMappingReturn { +export interface BuildRiskScoreFromMappingReturn { riskScore: RiskScore; riskScoreMeta: Meta; // TODO: Stricter types } +/** + * Calculates the final risk score for a detection alert based on: + * - source event object that can potentially contain fields representing risk score + * - the default score specified by the user + * - (optional) score mapping specified by the user ("map this field to the score") + * + * NOTE: Current MVP support is for mapping from a single field. + */ export const buildRiskScoreFromMapping = ({ eventSource, riskScore, riskScoreMapping, }: BuildRiskScoreFromMappingProps): BuildRiskScoreFromMappingReturn => { - // MVP support is for mapping from a single field - if (riskScoreMapping != null && riskScoreMapping.length > 0) { - const mappedField = riskScoreMapping[0].field; - // TODO: Expand by verifying fieldType from index via doc._index - const mappedValue = get(mappedField, eventSource); - if ( - typeof mappedValue === 'number' && - Number.isSafeInteger(mappedValue) && - mappedValue >= 0 && - mappedValue <= 100 - ) { - return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } }; + if (!riskScoreMapping || !riskScoreMapping.length) { + return defaultScore(riskScore); + } + + // TODO: Expand by verifying fieldType from index via doc._index + const eventField = riskScoreMapping[0].field; + const eventValue = get(eventField, eventSource); + const eventValues = Array.isArray(eventValue) ? eventValue : [eventValue]; + + const validNumbers = eventValues.map(toValidNumberOrMinusOne).filter((n) => n > -1); + + if (validNumbers.length > 0) { + const maxNumber = getMaxOf(validNumbers); + return overriddenScore(maxNumber); + } + + return defaultScore(riskScore); +}; + +function toValidNumberOrMinusOne(value: unknown): number { + if (typeof value === 'number' && isValidNumber(value)) { + return value; + } + + if (typeof value === 'string') { + const num = Number(value); + if (isValidNumber(num)) { + return num; } } + + return -1; +} + +function isValidNumber(value: number): boolean { + return Number.isFinite(value) && value >= 0 && value <= 100; +} + +function getMaxOf(array: number[]) { + // NOTE: It's safer to use reduce rather than Math.max(...array). The latter won't handle large input. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max + return array.reduce((a, b) => Math.max(a, b)); +} + +function defaultScore(riskScore: RiskScore): BuildRiskScoreFromMappingReturn { return { riskScore, riskScoreMeta: {} }; -}; +} + +function overriddenScore(riskScore: RiskScore): BuildRiskScoreFromMappingReturn { + return { riskScore, riskScoreMeta: { riskScoreOverridden: true } }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index 430564cd985c2..cfb5c56d7cd23 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -4,63 +4,169 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId, sampleDocSeverity } from '../__mocks__/es_results'; -import { buildSeverityFromMapping } from './build_severity_from_mapping'; +import { + Severity, + SeverityMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { sampleDocSeverity } from '../__mocks__/es_results'; +import { + buildSeverityFromMapping, + BuildSeverityFromMappingReturn, +} from './build_severity_from_mapping'; + +const ECS_FIELD = 'event.severity'; +const ANY_FIELD = 'event.my_custom_severity'; describe('buildSeverityFromMapping', () => { beforeEach(() => { jest.clearAllMocks(); }); - test('severity defaults to provided if mapping is undefined', () => { - const severity = buildSeverityFromMapping({ - eventSource: sampleDocNoSortId()._source, - severity: 'low', - severityMapping: undefined, + describe('base cases: when mapping is undefined', () => { + test('returns the provided default severity', () => { + testIt({ + fieldValue: 23, + severityDefault: 'low', + severityMapping: undefined, + expected: severityOf('low'), + }); + }); + }); + + describe('base cases: when mapping to the "event.severity" field from ECS', () => { + test(`severity is overridden if there's a match to a number`, () => { + testIt({ + fieldValue: 23, + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: '13', severity: 'low' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ECS_FIELD, operator: 'equals', value: '33', severity: 'high' }, + { field: ECS_FIELD, operator: 'equals', value: '43', severity: 'critical' }, + ], + expected: overriddenSeverityOf('medium'), + }); }); - expect(severity).toEqual({ severity: 'low', severityMeta: {} }); + test(`returns the default severity if there's a match to a string (ignores strings)`, () => { + testIt({ + fieldValue: 'hackerman', + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: 'hackerman', severity: 'critical' }, + ], + expected: severityOf('low'), + }); + }); }); - test('severity is overridden to highest matched mapping', () => { - const severity = buildSeverityFromMapping({ - eventSource: sampleDocSeverity(23)._source, - severity: 'low', - severityMapping: [ - { field: 'event.severity', operator: 'equals', value: '23', severity: 'critical' }, - { field: 'event.severity', operator: 'equals', value: '23', severity: 'low' }, - { field: 'event.severity', operator: 'equals', value: '11', severity: 'critical' }, - { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, - ], + describe('base cases: when mapping to any other field containing a single value', () => { + test(`severity is overridden if there's a match to a number`, () => { + testIt({ + fieldName: ANY_FIELD, + fieldValue: 23, + severityDefault: 'low', + severityMapping: [ + { field: ANY_FIELD, operator: 'equals', value: '13', severity: 'low' }, + { field: ANY_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ANY_FIELD, operator: 'equals', value: '33', severity: 'high' }, + { field: ANY_FIELD, operator: 'equals', value: '43', severity: 'critical' }, + ], + expected: overriddenSeverityOf('medium', ANY_FIELD), + }); }); - expect(severity).toEqual({ - severity: 'critical', - severityMeta: { - severityOverrideField: 'event.severity', - }, + test(`severity is overridden if there's a match to a string`, () => { + testIt({ + fieldName: ANY_FIELD, + fieldValue: 'hackerman', + severityDefault: 'low', + severityMapping: [ + { field: ANY_FIELD, operator: 'equals', value: 'anything', severity: 'medium' }, + { field: ANY_FIELD, operator: 'equals', value: 'hackerman', severity: 'critical' }, + ], + expected: overriddenSeverityOf('critical', ANY_FIELD), + }); }); }); - test('severity is overridden when field is event.severity and source value is number', () => { - const severity = buildSeverityFromMapping({ - eventSource: sampleDocSeverity(23)._source, - severity: 'low', - severityMapping: [ - { field: 'event.severity', operator: 'equals', value: '13', severity: 'low' }, - { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, - { field: 'event.severity', operator: 'equals', value: '33', severity: 'high' }, - { field: 'event.severity', operator: 'equals', value: '43', severity: 'critical' }, - ], + describe('base cases: when mapping to an array', () => { + test(`severity is overridden to highest matched mapping (works for "event.severity" field)`, () => { + testIt({ + fieldValue: [23, 'some string', 43, 33], + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: '13', severity: 'low' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ECS_FIELD, operator: 'equals', value: '33', severity: 'high' }, + { field: ECS_FIELD, operator: 'equals', value: '43', severity: 'critical' }, + ], + expected: overriddenSeverityOf('critical'), + }); }); - expect(severity).toEqual({ - severity: 'medium', - severityMeta: { - severityOverrideField: 'event.severity', - }, + test(`severity is overridden to highest matched mapping (works for any custom field)`, () => { + testIt({ + fieldName: ANY_FIELD, + fieldValue: ['foo', 'bar', 'baz', 'boo'], + severityDefault: 'low', + severityMapping: [ + { field: ANY_FIELD, operator: 'equals', value: 'bar', severity: 'high' }, + { field: ANY_FIELD, operator: 'equals', value: 'baz', severity: 'critical' }, + { field: ANY_FIELD, operator: 'equals', value: 'foo', severity: 'low' }, + { field: ANY_FIELD, operator: 'equals', value: 'boo', severity: 'medium' }, + ], + expected: overriddenSeverityOf('critical', ANY_FIELD), + }); }); }); - // TODO: Enhance... + describe('edge cases: when mapping the same numerical value to different severities multiple times', () => { + test('severity is overridden to highest matched mapping', () => { + testIt({ + fieldValue: 23, + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'critical' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'high' }, + ], + expected: overriddenSeverityOf('critical'), + }); + }); + }); }); + +interface TestCase { + fieldName?: string; + fieldValue: unknown; + severityDefault: Severity; + severityMapping: SeverityMappingOrUndefined; + expected: BuildSeverityFromMappingReturn; +} + +function testIt({ fieldName, fieldValue, severityDefault, severityMapping, expected }: TestCase) { + const result = buildSeverityFromMapping({ + eventSource: sampleDocSeverity(fieldValue, fieldName)._source, + severity: severityDefault, + severityMapping, + }); + + expect(result).toEqual(expected); +} + +function severityOf(value: Severity) { + return { + severity: value, + severityMeta: {}, + }; +} + +function overriddenSeverityOf(value: Severity, field = ECS_FIELD) { + return { + severity: value, + severityMeta: { + severityOverrideField: field, + }, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts index 52ebd67f257af..1560bbb48f0ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -11,15 +11,16 @@ import { severity as SeverityIOTS, SeverityMappingOrUndefined, } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SearchTypes } from '../../../../../common/detection_engine/types'; import { SignalSource } from '../types'; -interface BuildSeverityFromMappingProps { +export interface BuildSeverityFromMappingProps { eventSource: SignalSource; severity: Severity; severityMapping: SeverityMappingOrUndefined; } -interface BuildSeverityFromMappingReturn { +export interface BuildSeverityFromMappingReturn { severity: Severity; severityMeta: Meta; // TODO: Stricter types } @@ -31,41 +32,89 @@ const severitySortMapping = { critical: 3, }; +const ECS_SEVERITY_FIELD = 'event.severity'; + export const buildSeverityFromMapping = ({ eventSource, severity, severityMapping, }: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { - if (severityMapping != null && severityMapping.length > 0) { - let severityMatch: SeverityMappingItem | undefined; - - // Sort the SeverityMapping from low to high, so last match (highest severity) is used - const severityMappingSorted = severityMapping.sort( - (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] - ); - - severityMappingSorted.forEach((mapping) => { - const docValue = get(mapping.field, eventSource); - // TODO: Expand by verifying fieldType from index via doc._index - // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be - // another datatype, but until we can lookup datatype we must assume number for the Elastic - // Endpoint Security rule to function correctly - let parsedMappingValue: string | number = mapping.value; - if (mapping.field === 'event.severity') { - parsedMappingValue = Math.floor(Number(parsedMappingValue)); - } - - if (parsedMappingValue === docValue) { - severityMatch = { ...mapping }; - } - }); - - if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { - return { - severity: severityMatch.severity, - severityMeta: { severityOverrideField: severityMatch.field }, - }; + if (!severityMapping || !severityMapping.length) { + return defaultSeverity(severity); + } + + let severityMatch: SeverityMappingItem | undefined; + + // Sort the SeverityMapping from low to high, so last match (highest severity) is used + const severityMappingSorted = severityMapping.sort( + (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] + ); + + severityMappingSorted.forEach((mapping) => { + const mappingField = mapping.field; + const mappingValue = mapping.value; + const eventValue = get(mappingField, eventSource); + + const normalizedEventValues = normalizeEventValue(mappingField, eventValue); + const normalizedMappingValue = normalizeMappingValue(mappingField, mappingValue); + + if (normalizedEventValues.has(normalizedMappingValue)) { + severityMatch = { ...mapping }; } + }); + + if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { + return overriddenSeverity(severityMatch.severity, severityMatch.field); } - return { severity, severityMeta: {} }; + + return defaultSeverity(severity); }; + +function normalizeMappingValue(eventField: string, mappingValue: string): string | number { + // TODO: Expand by verifying fieldType from index via doc._index + // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be + // another datatype, but until we can lookup datatype we must assume number for the Elastic + // Endpoint Security rule to function correctly + if (eventField === ECS_SEVERITY_FIELD) { + return Math.floor(Number(mappingValue)); + } + + return mappingValue; +} + +function normalizeEventValue(eventField: string, eventValue: SearchTypes): Set { + const eventValues = Array.isArray(eventValue) ? eventValue : [eventValue]; + const validValues = eventValues.filter((v): v is string | number => isValidValue(eventField, v)); + const finalValues = eventField === ECS_SEVERITY_FIELD ? validValues : validValues.map(String); + return new Set(finalValues); +} + +function isValidValue(eventField: string, value: unknown): value is string | number { + return eventField === ECS_SEVERITY_FIELD + ? isValidNumber(value) + : isValidNumber(value) || isValidString(value); +} + +function isValidString(value: unknown): value is string { + return typeof value === 'string'; +} + +function isValidNumber(value: unknown): value is number { + return typeof value === 'number' && Number.isSafeInteger(value); +} + +function defaultSeverity(value: Severity): BuildSeverityFromMappingReturn { + return { + severity: value, + severityMeta: {}, + }; +} + +function overriddenSeverity(value: Severity, field: string): BuildSeverityFromMappingReturn { + return { + severity: value, + severityMeta: { + severityOverrideField: field, + }, + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 0db3013503a33..9442d911c3fd9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { orderBy } from 'lodash'; import { EqlCreateSchema, @@ -617,5 +618,157 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + /** + * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" + * in the code). If the rule specifies a mapping, then the final Severity or Risk Score + * value of the signal will be taken from the mapped field of the source event. + */ + describe('Signals generated from events with custom severity and risk score fields', () => { + beforeEach(async () => { + await esArchiver.load('signals/severity_risk_overrides'); + }); + + afterEach(async () => { + await esArchiver.unload('signals/severity_risk_overrides'); + }); + + const executeRuleAndGetSignals = async (rule: QueryCreateSchema) => { + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsResponse = await getSignalsByIds(supertest, [id]); + const signals = signalsResponse.hits.hits.map((hit) => hit._source); + const signalsOrderedByEventId = orderBy(signals, 'signal.parent.id', 'asc'); + return signalsOrderedByEventId; + }; + + it('should get default severity and risk score if there is no mapping', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + risk_score: 75, + }; + + const signals = await executeRuleAndGetSignals(rule); + + expect(signals.length).equal(4); + signals.forEach((s) => { + expect(s.signal.rule.severity).equal('medium'); + expect(s.signal.rule.severity_mapping).eql([]); + + expect(s.signal.rule.risk_score).equal(75); + expect(s.signal.rule.risk_score_mapping).eql([]); + }); + }); + + it('should get overridden severity if the rule has a mapping for it', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + severity_mapping: [ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ], + risk_score: 75, + }; + + const signals = await executeRuleAndGetSignals(rule); + const severities = signals.map((s) => ({ + id: s.signal.parent?.id, + value: s.signal.rule.severity, + })); + + expect(signals.length).equal(4); + expect(severities).eql([ + { id: '1', value: 'high' }, + { id: '2', value: 'critical' }, + { id: '3', value: 'critical' }, + { id: '4', value: 'critical' }, + ]); + + signals.forEach((s) => { + expect(s.signal.rule.risk_score).equal(75); + expect(s.signal.rule.risk_score_mapping).eql([]); + expect(s.signal.rule.severity_mapping).eql([ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ]); + }); + }); + + it('should get overridden risk score if the rule has a mapping for it', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + risk_score: 75, + risk_score_mapping: [ + { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, + ], + }; + + const signals = await executeRuleAndGetSignals(rule); + const riskScores = signals.map((s) => ({ + id: s.signal.parent?.id, + value: s.signal.rule.risk_score, + })); + + expect(signals.length).equal(4); + expect(riskScores).eql([ + { id: '1', value: 31.14 }, + { id: '2', value: 32.14 }, + { id: '3', value: 33.14 }, + { id: '4', value: 34.14 }, + ]); + + signals.forEach((s) => { + expect(s.signal.rule.severity).equal('medium'); + expect(s.signal.rule.severity_mapping).eql([]); + expect(s.signal.rule.risk_score_mapping).eql([ + { field: 'my_risk', operator: 'equals', value: '' }, + ]); + }); + }); + + it('should get overridden severity and risk score if the rule has both mappings', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + severity_mapping: [ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ], + risk_score: 75, + risk_score_mapping: [ + { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, + ], + }; + + const signals = await executeRuleAndGetSignals(rule); + const values = signals.map((s) => ({ + id: s.signal.parent?.id, + severity: s.signal.rule.severity, + risk: s.signal.rule.risk_score, + })); + + expect(signals.length).equal(4); + expect(values).eql([ + { id: '1', severity: 'high', risk: 31.14 }, + { id: '2', severity: 'critical', risk: 32.14 }, + { id: '3', severity: 'critical', risk: 33.14 }, + { id: '4', severity: 'critical', risk: 34.14 }, + ]); + + signals.forEach((s) => { + expect(s.signal.rule.severity_mapping).eql([ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ]); + expect(s.signal.rule.risk_score_mapping).eql([ + { field: 'my_risk', operator: 'equals', value: '' }, + ]); + }); + }); + }); }); }; diff --git a/x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json new file mode 100644 index 0000000000000..1f541dc1ef0a5 --- /dev/null +++ b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json @@ -0,0 +1,55 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:01.000Z", + "my_severity" : "sev_900", + "my_risk": 31.14 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:02.000Z", + "my_severity": ["sev_900", "sev_max"], + "my_risk": [32.14] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:03.000Z", + "my_severity": ["sev_max", "sev_900"], + "my_risk": "33.14" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:04.000Z", + "my_severity": "sev_max", + "my_risk": [3.14, "34.14"] + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json new file mode 100644 index 0000000000000..8a67be50e05fe --- /dev/null +++ b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json @@ -0,0 +1,26 @@ +{ + "type": "index", + "value": { + "index": "signal_overrides", + "mappings": { + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "my_severity": { + "type": "keyword" + }, + "my_risk": { + "type": "integer" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From c9fc876da110346501360751152d16c414ad5147 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Tue, 1 Dec 2020 15:00:15 +0100 Subject: [PATCH 017/107] Return early when parallel install process detected (#84190) --- .../services/epm/packages/_install_package.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 05f552b558205..1af7ce149dfc0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -5,7 +5,7 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { InstallablePackage, InstallSource } from '../../../../common'; +import { InstallablePackage, InstallSource, MAX_TIME_COMPLETE_INSTALL } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, @@ -46,15 +46,29 @@ export async function _installPackage({ installSource: InstallSource; }): Promise { const { name: pkgName, version: pkgVersion } = packageInfo; - // add the package installation to the saved object. - // if some installation already exists, just update install info + // if some installation already exists if (installedPkg) { - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installing', - install_started_at: new Date().toISOString(), - install_source: installSource, - }); + // if the installation is currently running, don't try to install + // instead, only return already installed assets + if ( + installedPkg.attributes.install_status === 'installing' && + Date.now() - Date.parse(installedPkg.attributes.install_started_at) < + MAX_TIME_COMPLETE_INSTALL + ) { + let assets: AssetReference[] = []; + assets = assets.concat(installedPkg.attributes.installed_es); + assets = assets.concat(installedPkg.attributes.installed_kibana); + return assets; + } else { + // if no installation is running, or the installation has been running longer than MAX_TIME_COMPLETE_INSTALL + // (it might be stuck) update the saved object and proceed + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installing', + install_started_at: new Date().toISOString(), + install_source: installSource, + }); + } } else { await createInstallation({ savedObjectsClient, From a9845c6fc2809a1d85a303aee6a4e07fefcd6581 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 1 Dec 2020 15:02:59 +0100 Subject: [PATCH 018/107] [Lens] (Accessibility) Focus mistakenly stops on righthand form (#84519) --- .../editor_frame/config_panel/config_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 93b4a4e3bea20..3d453cd078b7f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -88,7 +88,7 @@ function LayerPanels( const layerIds = activeVisualization.getLayerIds(visualizationState); return ( - + {layerIds.map((layerId, index) => ( Date: Tue, 1 Dec 2020 15:03:38 +0100 Subject: [PATCH 019/107] [Lens] (Accessibility) Improve landmarks in Lens (#84511) --- .../editor_frame/frame_layout.scss | 5 +- .../editor_frame/frame_layout.tsx | 47 +++++++++++++++---- .../workspace_panel_wrapper.tsx | 2 +- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss index ac52190dc7b0d..3599254a285b7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss @@ -12,15 +12,18 @@ } .lnsFrameLayout__pageContent { - display: flex; overflow: hidden; flex-grow: 1; + flex-direction: row; } .lnsFrameLayout__pageBody { @include euiScrollBar; min-width: $lnsPanelMinWidth + $euiSizeXL; overflow: hidden auto; + display: flex; + flex-direction: column; + flex: 1 1 100%; // Leave out bottom padding so the suggestions scrollbar stays flush to window edge // Leave out left padding so the left sidebar's focus states are visible outside of content bounds // This also means needing to add same amount of margin to page content and suggestion items diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index 6a0b2c3301119..8e19ceb898b55 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -7,7 +7,8 @@ import './frame_layout.scss'; import React from 'react'; -import { EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; export interface FrameLayoutProps { dataPanel: React.ReactNode; @@ -19,16 +20,46 @@ export interface FrameLayoutProps { export function FrameLayout(props: FrameLayoutProps) { return ( -
- {props.dataPanel} - + +
+ +

+ {i18n.translate('xpack.lens.section.dataPanelLabel', { + defaultMessage: 'Data panel', + })} +

+
+ {props.dataPanel} +
+
+ +

+ {i18n.translate('xpack.lens.section.workspaceLabel', { + defaultMessage: 'Visualization workspace', + })} +

+
{props.workspacePanel} {props.suggestionsPanel} - - +
+
+ +

+ {i18n.translate('xpack.lens.section.configPanelLabel', { + defaultMessage: 'Config panel', + })} +

+
{props.configPanel} - -
+ +
); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 33ddc23312a96..d9fbaa22a0388 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -102,7 +102,7 @@ export function WorkspacePanelWrapper({ -

+

{title || i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })}

From 7a779007bfb579337cd1e91f22c2a9f259fee285 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 1 Dec 2020 15:16:02 +0100 Subject: [PATCH 020/107] [Discover] Unskip doc table tests (#84564) --- test/functional/apps/discover/_doc_table.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 49b160cc70312..20fda144b338e 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - // Failing: See https://github.com/elastic/kibana/issues/82445 - describe.skip('discover doc table', function describeIndexTests() { + describe('discover doc table', function describeIndexTests() { const defaultRowsLimit = 50; const rowsHardLimit = 500; From 99ea48f401ee631822d37ee8cc709470418e14b3 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 1 Dec 2020 08:26:01 -0600 Subject: [PATCH 021/107] [Security Solution] [Cases] Cypress for case connector selector options (#80745) --- .../cypress/integration/cases.spec.ts | 11 +- .../cases_connector_options.spec.ts | 72 ++++++ .../security_solution/cypress/objects/case.ts | 220 ++++++++++++++++++ .../cypress/screens/case_details.ts | 10 + .../cypress/screens/edit_connector.ts | 29 +++ .../cypress/tasks/case_details.ts | 13 +- .../cypress/tasks/create_new_case.ts | 73 ++++-- 7 files changed, 412 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/screens/edit_connector.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index ec3887ad72625..b32402851ac7c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -39,7 +39,12 @@ import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases'; import { openCaseTimeline } from '../tasks/case_details'; -import { backToCases, createNewCaseWithTimeline } from '../tasks/create_new_case'; +import { + attachTimeline, + backToCases, + createCase, + fillCasesMandatoryfields, +} from '../tasks/create_new_case'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; @@ -57,7 +62,9 @@ describe('Cases', () => { it('Creates a new case with timeline and opens the timeline', () => { loginAndWaitForPageWithoutDateRange(CASES_URL); goToCreateNewCase(); - createNewCaseWithTimeline(case1); + fillCasesMandatoryfields(case1); + attachTimeline(case1); + createCase(); backToCases(); cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases'); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts new file mode 100644 index 0000000000000..f227042a0f9dc --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { + case1, + connectorIds, + mockConnectorsResponse, + executeResponses, + ibmResilientConnectorOptions, + jiraConnectorOptions, + serviceNowConnectorOpions, +} from '../objects/case'; +import { + createCase, + fillCasesMandatoryfields, + fillIbmResilientConnectorOptions, + fillJiraConnectorOptions, + fillServiceNowConnectorOptions, +} from '../tasks/create_new_case'; +import { goToCreateNewCase } from '../tasks/all_cases'; +import { deleteCase } from '../tasks/case_details'; +import { CASES_URL } from '../urls/navigation'; +import { CONNECTOR_CARD_DETAILS, CONNECTOR_TITLE } from '../screens/case_details'; + +describe('Cases connector incident fields', () => { + before(() => { + cy.server(); + cy.route('GET', '**/api/cases/configure/connectors/_find', mockConnectorsResponse); + cy.route2('POST', `**/api/actions/action/${connectorIds.jira}/_execute`, (req) => { + const response = + JSON.parse(req.body).params.subAction === 'issueTypes' + ? executeResponses.jira.issueTypes + : executeResponses.jira.fieldsByIssueType; + req.reply(JSON.stringify(response)); + }); + cy.route2('POST', `**/api/actions/action/${connectorIds.resilient}/_execute`, (req) => { + const response = + JSON.parse(req.body).params.subAction === 'incidentTypes' + ? executeResponses.resilient.incidentTypes + : executeResponses.resilient.severity; + req.reply(JSON.stringify(response)); + }); + }); + + after(() => { + deleteCase(); + }); + + it('Correct incident fields show when connector is changed', () => { + loginAndWaitForPageWithoutDateRange(CASES_URL); + goToCreateNewCase(); + fillCasesMandatoryfields(case1); + fillJiraConnectorOptions(jiraConnectorOptions); + fillServiceNowConnectorOptions(serviceNowConnectorOpions); + fillIbmResilientConnectorOptions(ibmResilientConnectorOptions); + createCase(); + + cy.get(CONNECTOR_TITLE).should('have.text', ibmResilientConnectorOptions.title); + cy.get(CONNECTOR_CARD_DETAILS).should( + 'have.text', + `${ + ibmResilientConnectorOptions.title + }Incident Types: ${ibmResilientConnectorOptions.incidentTypes.join(', ')}Severity: ${ + ibmResilientConnectorOptions.severity + }` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 084df31a604a3..01e9a9124ca88 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -21,6 +21,23 @@ export interface Connector { password: string; } +export interface JiraConnectorOptions { + issueType: string; + priority: string; +} + +export interface ServiceNowconnectorOptions { + urgency: string; + severity: string; + impact: string; +} + +export interface IbmResilientConnectorOptions { + title: string; + severity: string; + incidentTypes: string[]; +} + export const caseTimeline: TimelineWithId = { title: 'SIEM test', description: 'description', @@ -43,4 +60,207 @@ export const serviceNowConnector: Connector = { password: 'password', }; +export const jiraConnectorOptions: JiraConnectorOptions = { + issueType: '10006', + priority: 'High', +}; + +export const serviceNowConnectorOpions: ServiceNowconnectorOptions = { + urgency: '2', + severity: '1', + impact: '3', +}; + +export const ibmResilientConnectorOptions: IbmResilientConnectorOptions = { + title: 'Resilient', + severity: 'Medium', + incidentTypes: ['Communication error (fax; email)', 'Denial of Service'], +}; + export const TIMELINE_CASE_ID = '68248e00-f689-11ea-9ab2-59238b522856'; +export const connectorIds = { + jira: '000e5f86-08b0-4882-adfd-6df981d45c1b', + sn: '93a69ba3-3c31-4b4c-bf86-cc79a090f437', + resilient: 'a6a8dd7f-7e88-48fe-9b9f-70b668da8cbc', +}; + +export const mockConnectorsResponse = [ + { + id: connectorIds.jira, + actionTypeId: '.jira', + name: 'Jira', + config: { + incidentConfiguration: { + mapping: [ + { source: 'title', target: 'summary', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'overwrite' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ], + }, + isCaseOwned: true, + apiUrl: 'https://siem-kibana.atlassian.net', + projectKey: 'RJ', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: connectorIds.resilient, + actionTypeId: '.resilient', + name: 'Resilient', + config: { + incidentConfiguration: { + mapping: [ + { source: 'title', target: 'name', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'overwrite' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ], + }, + isCaseOwned: true, + apiUrl: 'https://ibm-resilient.siem.estc.dev', + orgId: '201', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: connectorIds.sn, + actionTypeId: '.servicenow', + name: 'ServiceNow', + config: { + incidentConfiguration: { + mapping: [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'overwrite' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ], + }, + isCaseOwned: true, + apiUrl: 'https://dev65287.service-now.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, +]; +export const executeResponses = { + jira: { + issueTypes: { + status: 'ok', + data: [ + { id: '10006', name: 'Task' }, + { id: '10007', name: 'Sub-task' }, + ], + actionId: connectorIds.jira, + }, + fieldsByIssueType: { + status: 'ok', + data: { + summary: { allowedValues: [], defaultValue: {} }, + issuetype: { + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10006', + id: '10006', + description: 'A small, distinct piece of work.', + iconUrl: + 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10318&avatarType=issuetype', + name: 'Task', + subtask: false, + avatarId: 10318, + }, + ], + defaultValue: {}, + }, + attachment: { allowedValues: [], defaultValue: {} }, + duedate: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + project: { + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10011', + id: '10011', + key: 'RJ', + name: 'Refactor Jira', + projectTypeKey: 'business', + simplified: false, + avatarUrls: { + '48x48': + 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10011&avatarId=10423', + '24x24': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10011&avatarId=10423', + '16x16': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10011&avatarId=10423', + '32x32': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10011&avatarId=10423', + }, + }, + ], + defaultValue: {}, + }, + assignee: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/1', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/highest.svg', + name: 'Highest', + id: '1', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/2', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/high.svg', + name: 'High', + id: '2', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/3', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/medium.svg', + name: 'Medium', + id: '3', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/4', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/low.svg', + name: 'Low', + id: '4', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/5', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/lowest.svg', + name: 'Lowest', + id: '5', + }, + ], + defaultValue: { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/3', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/medium.svg', + name: 'Medium', + id: '3', + }, + }, + timetracking: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + }, + actionId: connectorIds.jira, + }, + }, + resilient: { + incidentTypes: { + status: 'ok', + data: [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 21, name: 'Denial of Service' }, + ], + actionId: connectorIds.resilient, + }, + severity: { + status: 'ok', + data: [ + { id: 4, name: 'Low' }, + { id: 5, name: 'Medium' }, + { id: 6, name: 'High' }, + ], + actionId: connectorIds.resilient, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 7b995f5395543..02ec74aaed29c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CASE_ACTIONS_BTN = '[data-test-subj="property-actions-ellipses"]'; + export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]'; @@ -27,6 +29,14 @@ export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME = export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; +export const CONNECTOR_CARD_DETAILS = '[data-test-subj="settings-connector-card"]'; + +export const CONNECTOR_TITLE = '[data-test-subj="settings-connector-card"] span.euiTitle'; + +export const DELETE_CASE_BTN = '[data-test-subj="property-actions-trash"]'; + +export const DELETE_CASE_CONFIRMATION_BTN = '[data-test-subj="confirmModalConfirmButton"]'; + export const PARTICIPANTS = 1; export const REPORTER = 0; diff --git a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts new file mode 100644 index 0000000000000..c0ae4f94c541b --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connectorIds } from '../objects/case'; + +export const CONNECTOR_RESILIENT = `[data-test-subj="connector-settings-resilient"]`; + +export const CONNECTOR_SELECTOR = '[data-test-subj="dropdown-connectors"]'; + +export const SELECT_IMPACT = `[data-test-subj="impactSelect"]`; + +export const SELECT_INCIDENT_TYPE = `[data-test-subj="incidentTypeComboBox"] input[data-test-subj="comboBoxSearchInput"]`; + +export const SELECT_ISSUE_TYPE = `[data-test-subj="issueTypeSelect"]`; + +export const SELECT_JIRA = `[data-test-subj="dropdown-connector-${connectorIds.jira}"]`; + +export const SELECT_PRIORITY = `[data-test-subj="prioritySelect"]`; + +export const SELECT_RESILIENT = `[data-test-subj="dropdown-connector-${connectorIds.resilient}"]`; + +export const SELECT_SEVERITY = `[data-test-subj="severitySelect"]`; + +export const SELECT_SN = `[data-test-subj="dropdown-connector-${connectorIds.sn}"]`; + +export const SELECT_URGENCY = `[data-test-subj="urgencySelect"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/case_details.ts b/x-pack/plugins/security_solution/cypress/tasks/case_details.ts index 976d568ab3a91..51850997c3685 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/case_details.ts @@ -5,7 +5,18 @@ */ import { TIMELINE_TITLE } from '../screens/timeline'; -import { CASE_DETAILS_TIMELINE_LINK_MARKDOWN } from '../screens/case_details'; +import { + CASE_ACTIONS_BTN, + CASE_DETAILS_TIMELINE_LINK_MARKDOWN, + DELETE_CASE_BTN, + DELETE_CASE_CONFIRMATION_BTN, +} from '../screens/case_details'; + +export const deleteCase = () => { + cy.get(CASE_ACTIONS_BTN).first().click(); + cy.get(DELETE_CASE_BTN).click(); + cy.get(DELETE_CASE_CONFIRMATION_BTN).click(); +}; export const openCaseTimeline = () => { cy.get(CASE_DETAILS_TIMELINE_LINK_MARKDOWN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index f5013eed07d29..39654fd115a4a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestCase } from '../objects/case'; +import { + IbmResilientConnectorOptions, + JiraConnectorOptions, + ServiceNowconnectorOptions, + TestCase, +} from '../objects/case'; import { BACK_TO_CASES_BTN, @@ -16,34 +21,76 @@ import { TIMELINE_SEARCHBOX, TITLE_INPUT, } from '../screens/create_new_case'; +import { + CONNECTOR_RESILIENT, + CONNECTOR_SELECTOR, + SELECT_IMPACT, + SELECT_INCIDENT_TYPE, + SELECT_ISSUE_TYPE, + SELECT_JIRA, + SELECT_PRIORITY, + SELECT_RESILIENT, + SELECT_SEVERITY, + SELECT_SN, + SELECT_URGENCY, +} from '../screens/edit_connector'; export const backToCases = () => { cy.get(BACK_TO_CASES_BTN).click({ force: true }); }; -export const createNewCase = (newCase: TestCase) => { +export const fillCasesMandatoryfields = (newCase: TestCase) => { cy.get(TITLE_INPUT).type(newCase.name, { force: true }); newCase.tags.forEach((tag) => { cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); }); cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); - - cy.get(SUBMIT_BTN).click({ force: true }); - cy.get(LOADING_SPINNER).should('exist'); - cy.get(LOADING_SPINNER).should('not.exist'); }; -export const createNewCaseWithTimeline = (newCase: TestCase) => { - cy.get(TITLE_INPUT).type(newCase.name, { force: true }); - newCase.tags.forEach((tag) => { - cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); - }); - cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); - +export const attachTimeline = (newCase: TestCase) => { cy.get(INSERT_TIMELINE_BTN).click({ force: true }); cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); +}; +export const createCase = () => { cy.get(SUBMIT_BTN).click({ force: true }); cy.get(LOADING_SPINNER).should('exist'); cy.get(LOADING_SPINNER).should('not.exist'); }; + +export const fillJiraConnectorOptions = (jiraConnector: JiraConnectorOptions) => { + cy.get(CONNECTOR_SELECTOR).click({ force: true }); + cy.get(SELECT_JIRA).click({ force: true }); + cy.get(SELECT_ISSUE_TYPE).should('exist'); + + cy.get(SELECT_PRIORITY).should('exist'); + cy.get(SELECT_ISSUE_TYPE).select(jiraConnector.issueType); + cy.get(SELECT_PRIORITY).select(jiraConnector.priority); +}; + +export const fillServiceNowConnectorOptions = ( + serviceNowConnectorOpions: ServiceNowconnectorOptions +) => { + cy.get(CONNECTOR_SELECTOR).click({ force: true }); + cy.get(SELECT_SN).click({ force: true }); + cy.get(SELECT_SEVERITY).should('exist'); + cy.get(SELECT_URGENCY).should('exist'); + cy.get(SELECT_IMPACT).should('exist'); + cy.get(SELECT_URGENCY).select(serviceNowConnectorOpions.urgency); + cy.get(SELECT_SEVERITY).select(serviceNowConnectorOpions.severity); + cy.get(SELECT_IMPACT).select(serviceNowConnectorOpions.impact); +}; + +export const fillIbmResilientConnectorOptions = ( + ibmResilientConnector: IbmResilientConnectorOptions +) => { + cy.get(CONNECTOR_SELECTOR).click({ force: true }); + cy.get(SELECT_RESILIENT).click({ force: true }); + cy.get(SELECT_INCIDENT_TYPE).should('exist'); + cy.get(SELECT_SEVERITY).should('exist'); + ibmResilientConnector.incidentTypes.forEach((incidentType) => { + cy.get(SELECT_INCIDENT_TYPE).type(`${incidentType}{enter}`, { force: true }); + }); + cy.get(CONNECTOR_RESILIENT).click(); + cy.get(SELECT_SEVERITY).select(ibmResilientConnector.severity); +}; From 920e4fa280aaed62be7ebb6e3dc47b1a0720240c Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 1 Dec 2020 15:55:54 +0100 Subject: [PATCH 022/107] Update code-comments describing babel plugins (#84622) --- packages/kbn-babel-preset/common_preset.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/kbn-babel-preset/common_preset.js b/packages/kbn-babel-preset/common_preset.js index 8e2f1d207f3f4..b14dcd8971c31 100644 --- a/packages/kbn-babel-preset/common_preset.js +++ b/packages/kbn-babel-preset/common_preset.js @@ -28,18 +28,19 @@ const plugins = [ // See https://github.com/babel/proposals/issues/12 for progress require.resolve('@babel/plugin-proposal-class-properties'), - // Optional Chaining proposal is stage 3 (https://github.com/tc39/proposal-optional-chaining) + // Optional Chaining proposal is stage 4 (https://github.com/tc39/proposal-optional-chaining) // Need this since we are using TypeScript 3.7+ require.resolve('@babel/plugin-proposal-optional-chaining'), - // Nullish coalescing proposal is stage 3 (https://github.com/tc39/proposal-nullish-coalescing) + + // Nullish coalescing proposal is stage 4 (https://github.com/tc39/proposal-nullish-coalescing) // Need this since we are using TypeScript 3.7+ require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), - // Proposal is on stage 4 (https://github.com/tc39/proposal-export-ns-from) + // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) // Need this since we are using TypeScript 3.8+ require.resolve('@babel/plugin-proposal-export-namespace-from'), - // Proposal is on stage 4 (https://github.com/tc39/proposal-export-ns-from) + // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) // Need this since we are using TypeScript 3.9+ require.resolve('@babel/plugin-proposal-private-methods'), ]; From 0b5c55c59717f977ec39a7657aa6b570e19bf876 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 1 Dec 2020 07:10:09 -0800 Subject: [PATCH 023/107] Revert "[Alerting] renames Resolved action group to Recovered (#84123)" This reverts commit 7dcaff5dddb0389bd2e5b35b41d58006710afc91. --- .../alerts/common/builtin_action_groups.ts | 10 +-- .../alerts/server/alert_type_registry.test.ts | 14 ++--- .../tests/get_alert_instance_summary.test.ts | 2 +- ...rt_instance_summary_from_event_log.test.ts | 62 +++---------------- .../alert_instance_summary_from_event_log.ts | 5 +- x-pack/plugins/alerts/server/plugin.ts | 5 +- .../server/task_runner/task_runner.test.ts | 12 ++-- .../alerts/server/task_runner/task_runner.ts | 32 +++++----- .../inventory_metric_threshold_executor.ts | 4 +- .../metric_threshold_executor.test.ts | 6 +- .../metric_threshold_executor.ts | 4 +- .../public/application/constants/index.ts | 6 +- .../action_form.test.tsx | 27 ++++---- .../action_type_form.tsx | 10 +-- .../tests/alerting/list_alert_types.ts | 4 +- .../spaces_only/tests/alerting/alerts_base.ts | 16 ++--- .../spaces_only/tests/alerting/event_log.ts | 14 ++--- .../alerting/get_alert_instance_summary.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- 19 files changed, 90 insertions(+), 147 deletions(-) diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index d9c5ae613f787..d31f75357d370 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,13 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const RecoveredActionGroup: ActionGroup = { - id: 'recovered', - name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { - defaultMessage: 'Recovered', +export const ResolvedActionGroup: ActionGroup = { + id: 'resolved', + name: i18n.translate('xpack.alerts.builtinActionGroups.resolved', { + defaultMessage: 'Resolved', }), }; export function getBuiltinActionGroups(): ActionGroup[] { - return [RecoveredActionGroup]; + return [ResolvedActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index b04871a047e4b..8dc387f96addb 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -105,8 +105,8 @@ describe('register()', () => { name: 'Default', }, { - id: 'recovered', - name: 'Recovered', + id: 'resolved', + name: 'Resolved', }, ], defaultActionGroupId: 'default', @@ -117,7 +117,7 @@ describe('register()', () => { expect(() => registry.register(alertType)).toThrowError( new Error( - `Alert type [id="${alertType.id}"] cannot be registered. Action groups [recovered] are reserved by the framework.` + `Alert type [id="${alertType.id}"] cannot be registered. Action groups [resolved] are reserved by the framework.` ) ); }); @@ -229,8 +229,8 @@ describe('get()', () => { "name": "Default", }, Object { - "id": "recovered", - "name": "Recovered", + "id": "resolved", + "name": "Resolved", }, ], "actionVariables": Object { @@ -287,8 +287,8 @@ describe('list()', () => { "name": "Test Action Group", }, Object { - "id": "recovered", - "name": "Recovered", + "id": "resolved", + "name": "Resolved", }, ], "actionVariables": Object { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 0a764ea768591..9bd61c0fe66d2 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -122,7 +122,7 @@ describe('getAlertInstanceSummary()', () => { .addActiveInstance('instance-previously-active', 'action group B') .advanceTime(10000) .addExecute() - .addRecoveredInstance('instance-previously-active') + .addResolvedInstance('instance-previously-active') .addActiveInstance('instance-currently-active', 'action group A') .getEvents(); const eventsResult = { diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index 1d5ebe2b5911e..f9e4a2908d6ce 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary } from '../types'; import { IValidatedEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log'; const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; @@ -189,43 +189,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addRecoveredInstance('instance-1') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "OK", - } - `); - }); - - test('legacy alert with currently inactive instance', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addLegacyResolvedInstance('instance-1') + .addResolvedInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -260,7 +224,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addRecoveredInstance('instance-1') + .addResolvedInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -442,7 +406,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addRecoveredInstance('instance-2') + .addResolvedInstance('instance-2') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -487,7 +451,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addRecoveredInstance('instance-2') + .addResolvedInstance('instance-2') .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group B') @@ -597,24 +561,12 @@ export class EventsFactory { return this; } - addRecoveredInstance(instanceId: string): EventsFactory { - this.events.push({ - '@timestamp': this.date, - event: { - provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.recoveredInstance, - }, - kibana: { alerting: { instance_id: instanceId } }, - }); - return this; - } - - addLegacyResolvedInstance(instanceId: string): EventsFactory { + addResolvedInstance(instanceId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, - action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, + action: EVENT_LOG_ACTIONS.resolvedInstance, }, kibana: { alerting: { instance_id: instanceId } }, }); diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index 6fed8b4aa4ee6..8fed97a74435d 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types'; import { IEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; export interface AlertInstanceSummaryFromEventLogParams { alert: SanitizedAlert; @@ -80,8 +80,7 @@ export function alertInstanceSummaryFromEventLog( status.status = 'Active'; status.actionGroupId = event?.kibana?.alerting?.action_group_id; break; - case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: - case EVENT_LOG_ACTIONS.recoveredInstance: + case EVENT_LOG_ACTIONS.resolvedInstance: status.status = 'OK'; status.activeStartDate = undefined; status.actionGroupId = undefined; diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index bafb89c64076b..4bfb44425544a 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -82,11 +82,8 @@ export const EVENT_LOG_ACTIONS = { execute: 'execute', executeAction: 'execute-action', newInstance: 'new-instance', - recoveredInstance: 'recovered-instance', - activeInstance: 'active-instance', -}; -export const LEGACY_EVENT_LOG_ACTIONS = { resolvedInstance: 'resolved-instance', + activeInstance: 'active-instance', }; export interface PluginSetupContract { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index d4c4f746392c3..07d08f5837d54 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -26,12 +26,12 @@ import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { Alert, RecoveredActionGroup } from '../../common'; +import { Alert, ResolvedActionGroup } from '../../common'; import { omit } from 'lodash'; const alertType = { id: 'test', name: 'My test alert', - actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], + actionGroups: [{ id: 'default', name: 'Default' }, ResolvedActionGroup], defaultActionGroupId: 'default', executor: jest.fn(), producer: 'alerts', @@ -114,7 +114,7 @@ describe('Task Runner', () => { }, }, { - group: RecoveredActionGroup.id, + group: ResolvedActionGroup.id, id: '2', actionTypeId: 'action', params: { @@ -517,7 +517,7 @@ describe('Task Runner', () => { `); }); - test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { + test('fire resolved actions for execution for the alertInstances which is in the resolved state', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -650,7 +650,7 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "recovered-instance", + "action": "resolved-instance", }, "kibana": Object { "alerting": Object { @@ -666,7 +666,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' instance '2' has recovered", + "message": "test:1: 'alert-name' resolved instance: '2'", }, ], Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 5a7247ac50ea0..24d96788c3395 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -39,7 +39,7 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; import { AlertsClient } from '../alerts_client'; import { partiallyUpdateAlert } from '../saved_objects'; -import { RecoveredActionGroup } from '../../common'; +import { ResolvedActionGroup } from '../../common'; const FALLBACK_RETRY_INTERVAL = '5m'; @@ -219,7 +219,7 @@ export class TaskRunner { alertInstance.hasScheduledActions() ); - generateNewAndRecoveredInstanceEvents({ + generateNewAndResolvedInstanceEvents({ eventLogger, originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, @@ -229,7 +229,7 @@ export class TaskRunner { }); if (!muteAll) { - scheduleActionsForRecoveredInstances( + scheduleActionsForResolvedInstances( alertInstances, executionHandler, originalAlertInstances, @@ -436,7 +436,7 @@ export class TaskRunner { } } -interface GenerateNewAndRecoveredInstanceEventsParams { +interface GenerateNewAndResolvedInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; @@ -445,20 +445,18 @@ interface GenerateNewAndRecoveredInstanceEventsParams { namespace: string | undefined; } -function generateNewAndRecoveredInstanceEvents( - params: GenerateNewAndRecoveredInstanceEventsParams -) { +function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); - const recoveredIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); + const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); - for (const id of recoveredIds) { + for (const id of resolvedIds) { const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; - const message = `${params.alertLabel} instance '${id}' has recovered`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup); + const message = `${params.alertLabel} resolved instance: '${id}'`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); } for (const id of newIds) { @@ -498,7 +496,7 @@ function generateNewAndRecoveredInstanceEvents( } } -function scheduleActionsForRecoveredInstances( +function scheduleActionsForResolvedInstances( alertInstancesMap: Record, executionHandler: ReturnType, originalAlertInstances: Record, @@ -507,22 +505,22 @@ function scheduleActionsForRecoveredInstances( ) { const currentAlertInstanceIds = Object.keys(currentAlertInstances); const originalAlertInstanceIds = Object.keys(originalAlertInstances); - const recoveredIds = without( + const resolvedIds = without( originalAlertInstanceIds, ...currentAlertInstanceIds, ...mutedInstanceIds ); - for (const id of recoveredIds) { + for (const id of resolvedIds) { const instance = alertInstancesMap[id]; - instance.updateLastScheduledActions(RecoveredActionGroup.id); + instance.updateLastScheduledActions(ResolvedActionGroup.id); instance.unscheduleActions(); executionHandler({ - actionGroup: RecoveredActionGroup.id, + actionGroup: ResolvedActionGroup.id, context: {}, state: {}, alertInstanceId: id, }); - instance.scheduleActions(RecoveredActionGroup.id); + instance.scheduleActions(ResolvedActionGroup.id); } } diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 1941ec6326ddb..14785f64cffac 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; -import { RecoveredActionGroup } from '../../../../../alerts/common'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; @@ -103,7 +103,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group: item, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index a1d6428f3b52b..b31afba8ac9cc 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,7 +6,7 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; -import { RecoveredActionGroup } from '../../../../../alerts/common'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, @@ -367,7 +367,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('does not continue to send a recovery alert if the metric is still OK', async () => { @@ -383,7 +383,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 60790648d9a9b..7c3918c37ebbf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { RecoveredActionGroup } from '../../../../../alerts/common'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraBackendLibs } from '../../infra_types'; import { @@ -89,7 +89,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const firstResult = first(alertResults); const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 156f65f094342..7af8e5ba88300 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -16,10 +16,10 @@ export const routeToConnectors = `/connectors`; export const routeToAlerts = `/alerts`; export const routeToAlertDetails = `/alert/:alertId`; -export const recoveredActionGroupMessage = i18n.translate( - 'xpack.triggersActionsUI.sections.actionForm.RecoveredMessage', +export const resolvedActionGroupMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.ResolvedMessage', { - defaultMessage: 'Recovered', + defaultMessage: 'Resolved', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index ddbf933078043..5b56720737b7e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -10,9 +10,8 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; -import { RecoveredActionGroup } from '../../../../../alerts/common'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; import { useKibana } from '../../../common/lib/kibana'; -import { EuiScreenReaderOnly } from '@elastic/eui'; jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -229,7 +228,7 @@ describe('action_form', () => { }} actionGroups={[ { id: 'default', name: 'Default' }, - { id: 'recovered', name: 'Recovered' }, + { id: 'resolved', name: 'Resolved' }, ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; @@ -348,18 +347,18 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", - "inputDisplay": "Recovered", - "value": "recovered", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", + "inputDisplay": "Resolved", + "value": "resolved", }, ] `); }); - it('renders selected Recovered action group', async () => { + it('renders selected Resolved action group', async () => { const wrapper = await setup([ { - group: RecoveredActionGroup.id, + group: ResolvedActionGroup.id, id: 'test', actionTypeId: actionType.id, params: { @@ -382,17 +381,15 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", - "inputDisplay": "Recovered", - "value": "recovered", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", + "inputDisplay": "Resolved", + "value": "resolved", }, ] `); - - expect(actionGroupsSelect.first().find(EuiScreenReaderOnly).text()).toEqual( - 'Select an option: Recovered, is selected' + expect(actionGroupsSelect.first().text()).toEqual( + 'Select an option: Resolved, is selectedResolved' ); - expect(actionGroupsSelect.first().find('button').first().text()).toEqual('Recovered'); }); it('renders available connectors for the selected action type', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index fffc3bd32125e..7e38805957931 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -26,7 +26,7 @@ import { EuiBadge, EuiErrorBoundary, } from '@elastic/eui'; -import { AlertActionParam, RecoveredActionGroup } from '../../../../../alerts/common'; +import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; import { IErrorObject, AlertAction, @@ -40,7 +40,7 @@ import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_en import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; import { transformActionVariables } from '../../lib/action_variables'; -import { recoveredActionGroupMessage } from '../../constants'; +import { resolvedActionGroupMessage } from '../../constants'; import { useKibana } from '../../../common/lib/kibana'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; @@ -105,8 +105,8 @@ export const ActionTypeForm = ({ useEffect(() => { setAvailableActionVariables(getAvailableActionVariables(messageVariables, actionItem.group)); const res = - actionItem.group === RecoveredActionGroup.id - ? recoveredActionGroupMessage + actionItem.group === ResolvedActionGroup.id + ? resolvedActionGroupMessage : defaultActionMessage; setAvailableDefaultActionMessage(res); const paramsDefaults = getDefaultsForActionParams(actionItem.actionTypeId, actionItem.group); @@ -374,7 +374,7 @@ function getAvailableActionVariables( return []; } const filteredActionVariables = - actionGroup === RecoveredActionGroup.id + actionGroup === ResolvedActionGroup.id ? { params: actionVariables.params, state: actionVariables.state } : actionVariables; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index bfaf8a2a4788e..b3635b9f40e27 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -17,7 +17,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'recovered', name: 'Recovered' }, + { id: 'resolved', name: 'Resolved' }, ], defaultActionGroupId: 'default', id: 'test.noop', @@ -33,7 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedRestrictedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'recovered', name: 'Recovered' }, + { id: 'resolved', name: 'Resolved' }, ], defaultActionGroupId: 'default', id: 'test.restricted-noop', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 8dab199271da8..64e99190e183a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { RecoveredActionGroup } from '../../../../../plugins/alerts/common'; +import { ResolvedActionGroup } from '../../../../../plugins/alerts/common'; import { Space } from '../../../common/types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -137,7 +137,7 @@ instanceStateValue: true await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); }); - it('should fire actions when an alert instance is recovered', async () => { + it('should fire actions when an alert instance is resolved', async () => { const reference = alertUtils.generateReference(); const { body: createdAction } = await supertestWithoutAuth @@ -174,12 +174,12 @@ instanceStateValue: true params: {}, }, { - group: RecoveredActionGroup.id, + group: ResolvedActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Recovered message', + message: 'Resolved message', }, }, ], @@ -194,10 +194,10 @@ instanceStateValue: true await esTestIndexTool.waitForDocs('action:test.index-record', reference) )[0]; - expect(actionTestRecord._source.params.message).to.eql('Recovered message'); + expect(actionTestRecord._source.params.message).to.eql('Resolved message'); }); - it('should not fire actions when an alert instance is recovered, but alert is muted', async () => { + it('should not fire actions when an alert instance is resolved, but alert is muted', async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); @@ -237,12 +237,12 @@ instanceStateValue: true params: {}, }, { - group: RecoveredActionGroup.id, + group: ResolvedActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Recovered message', + message: 'Resolved message', }, }, ], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 3766785680925..d3cd3db124ecd 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -78,7 +78,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { 'execute-action', 'new-instance', 'active-instance', - 'recovered-instance', + 'resolved-instance', ], }); }); @@ -87,25 +87,25 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const executeEvents = getEventsByAction(events, 'execute'); const executeActionEvents = getEventsByAction(events, 'execute-action'); const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); + const resolvedInstanceEvents = getEventsByAction(events, 'resolved-instance'); expect(executeEvents.length >= 4).to.be(true); expect(executeActionEvents.length).to.be(2); expect(newInstanceEvents.length).to.be(1); - expect(recoveredInstanceEvents.length).to.be(1); + expect(resolvedInstanceEvents.length).to.be(1); // make sure the events are in the right temporal order const executeTimes = getTimestamps(executeEvents); const executeActionTimes = getTimestamps(executeActionEvents); const newInstanceTimes = getTimestamps(newInstanceEvents); - const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); + const resolvedInstanceTimes = getTimestamps(resolvedInstanceEvents); expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); // validate each event let executeCount = 0; @@ -136,8 +136,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { case 'new-instance': validateInstanceEvent(event, `created new instance: 'instance'`); break; - case 'recovered-instance': - validateInstanceEvent(event, `recovered instance: 'instance'`); + case 'resolved-instance': + validateInstanceEvent(event, `resolved instance: 'instance'`); break; case 'active-instance': validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 404c6020fa237..22034328e5275 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -216,7 +216,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr await alertUtils.muteInstance(createdAlert.id, 'instanceC'); await alertUtils.muteInstance(createdAlert.id, 'instanceD'); - await waitForEvents(createdAlert.id, ['new-instance', 'recovered-instance']); + await waitForEvents(createdAlert.id, ['new-instance', 'resolved-instance']); const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_instance_summary` ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 9d38f4abb7f3f..3fb2cc40437d8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -25,7 +25,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType).to.eql({ actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'recovered', name: 'Recovered' }, + { id: 'resolved', name: 'Resolved' }, ], defaultActionGroupId: 'default', id: 'test.noop', From 6e80d9fe09e82c59853a18146dcc96b684bbd22c Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 1 Dec 2020 10:15:51 -0500 Subject: [PATCH 024/107] [Security Solution] [Detections] Create a 'partial failure' status for rules (#84293) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../schemas/common/schemas.ts | 7 ++++- .../components/rules/rule_status/helpers.ts | 2 +- .../detection_engine/rules/types.ts | 11 +++++++- .../detection_engine/rules/details/index.tsx | 27 ++++++++++++++----- .../details/status_failed_callout.test.tsx | 7 +++++ .../rules/details/status_failed_callout.tsx | 8 ++++-- .../rules/details/translations.ts | 7 +++++ .../signals/rule_status_service.mock.ts | 2 ++ .../signals/rule_status_service.test.ts | 15 +++++++++++ .../signals/rule_status_service.ts | 20 ++++++++++++++ 10 files changed, 95 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 82b803c62a940..ff76a0fcb5593 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -320,7 +320,12 @@ export type SeverityMappingOrUndefined = t.TypeOf; -export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null }); +export const job_status = t.keyof({ + succeeded: null, + failed: null, + 'going to run': null, + 'partial failure': null, +}); export type JobStatus = t.TypeOf; export const conflicts = t.keyof({ abort: null, proceed: null }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts index e99894afeb63c..e6482577f89ed 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts @@ -13,6 +13,6 @@ export const getStatusColor = (status: RuleStatusType | string | null) => ? 'success' : status === 'failed' ? 'danger' - : status === 'executing' || status === 'going to run' + : status === 'executing' || status === 'going to run' || status === 'partial failure' ? 'warning' : 'subdued'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index e9c89130736c0..d7908a6780ebb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -70,6 +70,13 @@ const MetaRule = t.intersection([ }), ]); +const StatusTypes = t.union([ + t.literal('succeeded'), + t.literal('failed'), + t.literal('going to run'), + t.literal('partial failure'), +]); + export const RuleSchema = t.intersection([ t.type({ author, @@ -108,13 +115,15 @@ export const RuleSchema = t.intersection([ license, last_failure_at: t.string, last_failure_message: t.string, + last_success_message: t.string, + last_success_at: t.string, meta: MetaRule, machine_learning_job_id: t.string, output_index: t.string, query: t.string, rule_name_override, saved_id: t.string, - status: t.string, + status: StatusTypes, status_date: t.string, threshold, threat_query, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index ba676835d60f1..4866824f882cf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -275,18 +275,33 @@ export const RuleDetailsPageComponent: FC = ({ ), [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] ); - const ruleError = useMemo( - () => + const ruleError = useMemo(() => { + if ( rule?.status === 'failed' && ruleDetailTab === RuleDetailTabs.alerts && - rule?.last_failure_at != null ? ( + rule?.last_failure_at != null + ) { + return ( - ) : null, - [rule, ruleDetailTab] - ); + ); + } else if ( + rule?.status === 'partial failure' && + ruleDetailTab === RuleDetailTabs.alerts && + rule?.last_success_at != null + ) { + return ( + + ); + } + return null; + }, [rule, ruleDetailTab]); const updateDateRangeCallback = useCallback( ({ x }) => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx index 3394b0fc8c5c0..c743623402063 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx @@ -13,6 +13,13 @@ describe('RuleStatusFailedCallOut', () => { it('renders correctly', () => { const wrapper = shallow(); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); + it('renders correctly with optional params', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx index 5b5b96ace8670..121ff6b8686c3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx @@ -13,22 +13,26 @@ import * as i18n from './translations'; interface RuleStatusFailedCallOutComponentProps { date: string; message: string; + color?: 'danger' | 'primary' | 'success' | 'warning'; } const RuleStatusFailedCallOutComponent: React.FC = ({ date, message, + color, }) => ( - {i18n.ERROR_CALLOUT_TITLE} + + {color === 'warning' ? i18n.PARTIAL_FAILURE_CALLOUT_TITLE : i18n.ERROR_CALLOUT_TITLE} +
} - color="danger" + color={color ? color : 'danger'} iconType="alert" >

{message}

diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 94dfdc3e9daa0..5fbe0a5b78671 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -48,6 +48,13 @@ export const ERROR_CALLOUT_TITLE = i18n.translate( } ); +export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle', + { + defaultMessage: 'Partial rule failure at', + } +); + export const FAILURE_HISTORY_TAB = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.failureHistoryTab', { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts index 1d6a8227ebc13..f7b1790e127d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts @@ -22,6 +22,8 @@ export const ruleStatusServiceFactoryMock = async ({ success: jest.fn(), + partialFailure: jest.fn(), + error: jest.fn(), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts index cde6a506c657d..449ecd11257d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts @@ -53,6 +53,21 @@ describe('buildRuleStatusAttributes', () => { expect(result.statusDate).toEqual(result.lastSuccessAt); }); + it('returns partial failure fields if "partial failure"', () => { + const result = buildRuleStatusAttributes( + 'partial failure', + 'some indices missing timestamp override field' + ); + expect(result).toEqual({ + status: 'partial failure', + statusDate: expectIsoDateString, + lastSuccessAt: expectIsoDateString, + lastSuccessMessage: 'some indices missing timestamp override field', + }); + + expect(result.statusDate).toEqual(result.lastSuccessAt); + }); + it('returns failure fields if "failed"', () => { const result = buildRuleStatusAttributes('failed', 'failure message'); expect(result).toEqual({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 433ad4e2affea..debc329bf40d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -23,6 +23,7 @@ interface Attributes { export interface RuleStatusService { goingToRun: () => Promise; success: (message: string, attributes?: Attributes) => Promise; + partialFailure: (message: string, attributes?: Attributes) => Promise; error: (message: string, attributes?: Attributes) => Promise; } @@ -46,6 +47,13 @@ export const buildRuleStatusAttributes: ( lastSuccessMessage: message, }; } + case 'partial failure': { + return { + ...baseAttributes, + lastSuccessAt: now, + lastSuccessMessage: message, + }; + } case 'failed': { return { ...baseAttributes, @@ -93,6 +101,18 @@ export const ruleStatusServiceFactory = async ({ }); }, + partialFailure: async (message, attributes) => { + const [currentStatus] = await getOrCreateRuleStatuses({ + alertId, + ruleStatusClient, + }); + + await ruleStatusClient.update(currentStatus.id, { + ...currentStatus.attributes, + ...buildRuleStatusAttributes('partial failure', message, attributes), + }); + }, + error: async (message, attributes) => { const ruleStatuses = await getOrCreateRuleStatuses({ alertId, From a65c12ffa2604ea625c0cc9944314b80ca63d48c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 1 Dec 2020 11:01:06 -0500 Subject: [PATCH 025/107] Fix flaky test suite (#84602) --- test/api_integration/apis/saved_objects/migrations.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index b5fa16558514a..fa9c2fd1a2d7f 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -54,8 +54,7 @@ function getLogMock() { export default ({ getService }: FtrProviderContext) => { const esClient = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/84445 - describe.skip('Kibana index migration', () => { + describe('Kibana index migration', () => { before(() => esClient.indices.delete({ index: '.migrate-*' })); it('Migrates an existing index that has never been migrated before', async () => { @@ -313,7 +312,10 @@ export default ({ getService }: FtrProviderContext) => { result // @ts-expect-error destIndex exists only on MigrationResult status: 'migrated'; .map(({ status, destIndex }) => ({ status, destIndex })) - .sort((a) => (a.destIndex ? 0 : 1)) + .sort(({ destIndex: a }, { destIndex: b }) => + // sort by destIndex in ascending order, keeping falsy values at the end + (a && !b) || a < b ? -1 : (!a && b) || a > b ? 1 : 0 + ) ).to.eql([ { status: 'migrated', destIndex: '.migration-c_2' }, { status: 'skipped', destIndex: undefined }, From 636f91c29a5585ddf17bdb232eb561a554f37c03 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Dec 2020 18:01:37 +0200 Subject: [PATCH 026/107] [Security Solution][Detections] Fix labels and issue with mandatory fields (#84525) * Fix labeling and bugs * Improve naming --- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - .../components/builtin_action_types/jira/jira.test.tsx | 2 +- .../components/builtin_action_types/jira/jira.tsx | 4 ++-- .../components/builtin_action_types/jira/translations.ts | 6 +++--- .../builtin_action_types/resilient/resilient.test.tsx | 2 +- .../builtin_action_types/resilient/resilient.tsx | 5 +++-- .../builtin_action_types/resilient/translations.ts | 8 ++++---- .../builtin_action_types/servicenow/servicenow.test.tsx | 2 +- .../builtin_action_types/servicenow/servicenow.tsx | 2 +- .../builtin_action_types/servicenow/translations.ts | 2 +- 11 files changed, 17 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 490bf56d76062..4991c4d16099c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19600,7 +19600,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "説明が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField": "タイトルが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRAは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "オブジェクトID(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "親問題を選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 15699c3f47dd5..59ab890cfd9db 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19619,7 +19619,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "“描述”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "“电子邮件”或“用户名”必填", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "“项目键”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField": "“标题”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRA 将此操作与 Kibana 已保存对象的 ID 关联。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "对象 ID(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "选择父问题", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index f476522c2bf5a..b10341fa00f1b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -93,7 +93,7 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Title is required.'], + title: ['Summary is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 81f0bbfe8a02f..20374cfbe3a3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -64,8 +64,8 @@ export function getActionType(): ActionTypeModel(), }; validationResult.errors = errors; - if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { - errors.title.push(i18n.TITLE_REQUIRED); + if (!actionParams.subActionParams?.title?.length) { + errors.title.push(i18n.SUMMARY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 6f45316ff4433..c9642da9ba440 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -127,10 +127,10 @@ export const DESCRIPTION_REQUIRED = i18n.translate( } ); -export const TITLE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField', +export const SUMMARY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField', { - defaultMessage: 'Title is required.', + defaultMessage: 'Summary is required.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 937fe61e887ea..17e9b42e7878e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -93,7 +93,7 @@ describe('resilient action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Title is required.'], + title: ['Name is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index 6d57fc98fe20f..251274a08ba6c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -72,8 +72,9 @@ export function getActionType(): ActionTypeModel< title: new Array(), }; validationResult.errors = errors; - if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { - errors.title.push(i18n.TITLE_REQUIRED); + + if (!actionParams.subActionParams?.title?.length) { + errors.title.push(i18n.NAME_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts index 65d08c9f7de68..7483ba2f461df 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -134,16 +134,16 @@ export const MAPPING_FIELD_COMMENTS = i18n.translate( ); export const DESCRIPTION_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField', + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField', { defaultMessage: 'Description is required.', } ); -export const TITLE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', +export const NAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField', { - defaultMessage: 'Title is required.', + defaultMessage: 'Name is required.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index 5e70bc20f5c51..c29ddbf385de6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -90,7 +90,7 @@ describe('servicenow action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Title is required.'], + title: ['Short description is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 9cc689d8f48b1..8eca7f3ef3120 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -67,7 +67,7 @@ export function getActionType(): ActionTypeModel< title: new Array(), }; validationResult.errors = errors; - if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + if (!actionParams.subActionParams?.title?.length) { errors.title.push(i18n.TITLE_REQUIRED); } return validationResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 312cb9844bd75..91a5c0a54397b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -157,6 +157,6 @@ export const DESCRIPTION_REQUIRED = i18n.translate( export const TITLE_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', { - defaultMessage: 'Title is required.', + defaultMessage: 'Short description is required.', } ); From 5420177485796793d57b6439853caa1c05513433 Mon Sep 17 00:00:00 2001 From: Olivier V Date: Tue, 1 Dec 2020 17:06:19 +0100 Subject: [PATCH 027/107] Update create.asciidoc (#84046) The url templates provided for call of the API with space information was missing the /api/ section in it. (cherry picked from commit 35f1cc16eaa29666d7212402f57ab17858ebc96d) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/api/saved-objects/create.asciidoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index 50809a1bd5d4e..d7a368034ef07 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -9,11 +9,13 @@ experimental[] Create {kib} saved objects. [[saved-objects-api-create-request]] ==== Request -`POST :/api/saved_objects/` + +`POST :/api/saved_objects/` `POST :/api/saved_objects//` -`POST :/s//saved_objects/` +`POST :/s//api/saved_objects/` + +`POST :/s//api/saved_objects//` [[saved-objects-api-create-path-params]] ==== Path parameters From 5889e366da02a910e3bb3c5c3878e16ca661947b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 1 Dec 2020 18:05:57 +0100 Subject: [PATCH 028/107] [ML] Fix unnecessary trigger of wildcard field type search for ML plugin routes. (#84605) Passing in an empty string '' to useResolver() would trigger a wild card search across all indices and fields, potentially causing a timeout and the page would fail to load. The following pages were affected: Single Metric Viewer, Data frame analytics models list, Data frame analytics jobs list, Data frame analytics exploration page, File Data Visualizer (Data visualizer - Import data from a log file). This PR fixes it by passing undefined instead of '' to useResolver to avoid calling _fields_for_wildcard with an empty pattern. Jest tests were added to cover the two parameter scenarios empty string/undefined. --- .../jobs/new_job/utils/new_job_utils.ts | 2 +- .../analytics_job_exploration.tsx | 2 +- .../analytics_jobs_list.tsx | 2 +- .../data_frame_analytics/analytics_map.tsx | 2 +- .../data_frame_analytics/models_list.tsx | 2 +- .../routes/datavisualizer/file_based.tsx | 2 +- .../routing/routes/timeseriesexplorer.tsx | 2 +- .../application/routing/use_resolver.test.ts | 89 +++++++++++++++++++ .../application/routing/use_resolver.ts | 76 ++++++++++------ .../ml/public/application/util/index_utils.ts | 7 +- 10 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/routing/use_resolver.test.ts diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index b7b6aa15ffe44..543e6898193a4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -30,7 +30,7 @@ export function getDefaultDatafeedQuery() { export function createSearchItems( kibanaConfig: IUiSettingsClient, - indexPattern: IIndexPattern, + indexPattern: IIndexPattern | undefined, savedSearch: SavedSearchSavedObject | null ) { // query is only used by the data visualizer as it needs diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index e436ff468ccf0..771123532dcbf 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -38,7 +38,7 @@ export const analyticsJobExplorationRouteFactory = ( }); const PageWrapper: FC = ({ location, deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); const [globalState] = useUrlState('_g'); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 80706a82121d5..3b68c5078e99e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -34,7 +34,7 @@ export const analyticsJobsListRouteFactory = ( }); const PageWrapper: FC = ({ location, deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx index 18002648cfaa6..3acd12402932f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx @@ -34,7 +34,7 @@ export const analyticsMapRouteFactory = ( }); const PageWrapper: FC = ({ deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx index b1fd6e93a744c..2f58ef756e51c 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx @@ -34,7 +34,7 @@ export const modelsListRouteFactory = ( }); const PageWrapper: FC = ({ location, deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 837616a8a76d2..f651d17e02de4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -45,7 +45,7 @@ export const fileBasedRouteFactory = ( const PageWrapper: FC = ({ location, deps }) => { const { redirectToMlAccessDeniedPage } = deps; - const { context } = useResolver('', undefined, deps.config, { + const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkFindFileStructurePrivilege: () => diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index df92c77252565..7de59cba495af 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -63,7 +63,7 @@ export const timeSeriesExplorerRouteFactory = ( }); const PageWrapper: FC = ({ deps }) => { - const { context, results } = useResolver('', undefined, deps.config, { + const { context, results } = useResolver(undefined, undefined, deps.config, { ...basicResolvers(deps), jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts new file mode 100644 index 0000000000000..07cc038538745 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { IUiSettingsClient } from 'kibana/public'; + +import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; +import { useNotifications } from '../contexts/kibana'; + +import { useResolver } from './use_resolver'; + +jest.mock('../contexts/kibana/use_create_url', () => { + return { + useCreateAndNavigateToMlLink: jest.fn(), + }; +}); + +jest.mock('../contexts/kibana', () => { + return { + useMlUrlGenerator: () => ({ + createUrl: jest.fn(), + }), + useNavigateToPath: () => jest.fn(), + useNotifications: jest.fn(), + }; +}); + +const addError = jest.fn(); +(useNotifications as jest.Mock).mockImplementation(() => ({ + toasts: { addSuccess: jest.fn(), addDanger: jest.fn(), addError }, +})); + +const redirectToJobsManagementPage = jest.fn(() => Promise.resolve()); +(useCreateAndNavigateToMlLink as jest.Mock).mockImplementation(() => redirectToJobsManagementPage); + +describe('useResolver', () => { + afterEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.advanceTimersByTime(0); + jest.useRealTimers(); + }); + + it('should accept undefined as indexPatternId and savedSearchId.', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useResolver(undefined, undefined, {} as IUiSettingsClient, {}) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(result.current).toStrictEqual({ + context: { + combinedQuery: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + currentIndexPattern: null, + currentSavedSearch: null, + indexPatterns: null, + kibanaConfig: {}, + }, + results: {}, + }); + expect(addError).toHaveBeenCalledTimes(0); + expect(redirectToJobsManagementPage).toHaveBeenCalledTimes(0); + }); + + it('should add an error toast and redirect if indexPatternId is an empty string.', async () => { + const { result } = renderHook(() => useResolver('', undefined, {} as IUiSettingsClient, {})); + + await act(async () => {}); + + expect(result.current).toStrictEqual({ context: null, results: {} }); + expect(addError).toHaveBeenCalledTimes(1); + expect(redirectToJobsManagementPage).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.ts index e4cd90145bee4..3ce23f8b8a19c 100644 --- a/x-pack/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.ts @@ -11,6 +11,7 @@ import { getIndexPatternById, getIndexPatternsContract, getIndexPatternAndSavedSearch, + IndexPatternAndSavedSearch, } from '../util/index_utils'; import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; import { ResolverResults, Resolvers } from './resolvers'; @@ -19,6 +20,14 @@ import { useNotifications } from '../contexts/kibana'; import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; import { ML_PAGES } from '../../../common/constants/ml_url_generator'; +/** + * Hook to resolve route specific requirements + * @param indexPatternId optional Kibana index pattern id, used for wizards + * @param savedSearchId optional Kibana saved search id, used for wizards + * @param config Kibana UI Settings + * @param resolvers an array of resolvers to be executed for the route + * @return { context, results } returns the ML context and resolver results + */ export const useResolver = ( indexPatternId: string | undefined, savedSearchId: string | undefined, @@ -52,36 +61,49 @@ export const useResolver = ( return; } - if (indexPatternId !== undefined || savedSearchId !== undefined) { - try { - // note, currently we're using our own kibana context that requires a current index pattern to be set - // this means, if the page uses this context, useResolver must be passed a string for the index pattern id - // and loadIndexPatterns must be part of the resolvers. - const { indexPattern, savedSearch } = - savedSearchId !== undefined - ? await getIndexPatternAndSavedSearch(savedSearchId) - : { savedSearch: null, indexPattern: await getIndexPatternById(indexPatternId!) }; + try { + if (indexPatternId === '') { + throw new Error( + i18n.translate('xpack.ml.useResolver.errorIndexPatternIdEmptyString', { + defaultMessage: 'indexPatternId must not be empty string.', + }) + ); + } - const { combinedQuery } = createSearchItems(config, indexPattern!, savedSearch); + let indexPatternAndSavedSearch: IndexPatternAndSavedSearch = { + savedSearch: null, + indexPattern: null, + }; - setContext({ - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns: getIndexPatternsContract()!, - kibanaConfig: config, - }); - } catch (error) { - // an unexpected error has occurred. This could be caused by an incorrect index pattern or saved search ID - notifications.toasts.addError(new Error(error), { - title: i18n.translate('xpack.ml.useResolver.errorTitle', { - defaultMessage: 'An error has occurred', - }), - }); - await redirectToJobsManagementPage(); + if (savedSearchId !== undefined) { + indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(savedSearchId); + } else if (indexPatternId !== undefined) { + indexPatternAndSavedSearch.indexPattern = await getIndexPatternById(indexPatternId); } - } else { - setContext({}); + + const { savedSearch, indexPattern } = indexPatternAndSavedSearch; + + const { combinedQuery } = createSearchItems( + config, + indexPattern !== null ? indexPattern : undefined, + savedSearch + ); + + setContext({ + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns: getIndexPatternsContract(), + kibanaConfig: config, + }); + } catch (error) { + // an unexpected error has occurred. This could be caused by an incorrect index pattern or saved search ID + notifications.toasts.addError(new Error(error), { + title: i18n.translate('xpack.ml.useResolver.errorTitle', { + defaultMessage: 'An error has occurred', + }), + }); + await redirectToJobsManagementPage(); } })(); }, []); diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index 42be3dd8252f9..de08553af9906 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -73,9 +73,12 @@ export function getIndexPatternIdFromName(name: string) { } return null; } - +export interface IndexPatternAndSavedSearch { + savedSearch: SavedSearchSavedObject | null; + indexPattern: IIndexPattern | null; +} export async function getIndexPatternAndSavedSearch(savedSearchId: string) { - const resp: { savedSearch: SavedSearchSavedObject | null; indexPattern: IIndexPattern | null } = { + const resp: IndexPatternAndSavedSearch = { savedSearch: null, indexPattern: null, }; From 3958cddca3b76f351da552639a9448bb9874a5cc Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 1 Dec 2020 10:09:20 -0700 Subject: [PATCH 029/107] Remove unscripted fields from sample data index-pattern saved objects (#84659) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../services/sample_data/data_sets/ecommerce/saved_objects.ts | 3 +-- .../services/sample_data/data_sets/flights/saved_objects.ts | 2 +- .../services/sample_data/data_sets/logs/saved_objects.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 37657912deb95..60d05890028d1 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -294,8 +294,7 @@ export const getSavedObjects = (): SavedObject[] => [ attributes: { title: 'kibana_sample_data_ecommerce', timeFieldName: 'order_date', - fields: - '[{"name":"_id","type":"string","esTypes":["_id"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_index","type":"string","esTypes":["_index"],"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","esTypes":["_source"],"count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_type","type":"string","esTypes":["_type"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"category","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"category.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"category"}}},{"name":"currency","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_birth_date","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_first_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_first_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_first_name"}}},{"name":"customer_full_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_full_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_full_name"}}},{"name":"customer_gender","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_id","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_last_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_last_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_last_name"}}},{"name":"customer_phone","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"day_of_week","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"day_of_week_i","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"email","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"event.dataset","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.city_name","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.continent_name","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.country_iso_code","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.location","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.region_name","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"manufacturer","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"manufacturer.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"manufacturer"}}},{"name":"order_date","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"order_id","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products._id","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products._id.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products._id"}}},{"name":"products.base_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.base_unit_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.category","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products.category.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.category"}}},{"name":"products.created_on","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.discount_amount","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.discount_percentage","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.manufacturer","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products.manufacturer.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.manufacturer"}}},{"name":"products.min_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.product_id","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.product_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products.product_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.product_name"}}},{"name":"products.quantity","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.sku","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.tax_amount","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.taxful_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.taxless_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.unit_discount_amount","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"sku","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"taxful_total_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"taxless_total_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"total_quantity","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"total_unique_products","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"type","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"user","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + fields: '[]', fieldFormatMap: '{"taxful_total_price":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', }, references: [], diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 6f701d75e7d52..e65b6ad40651b 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -439,7 +439,7 @@ export const getSavedObjects = (): SavedObject[] => [ title: 'kibana_sample_data_flights', timeFieldName: 'timestamp', fields: - '[{"name":"AvgTicketPrice","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Cancelled","type":"boolean","esTypes":["boolean"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Carrier","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Dest","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestAirportID","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestCityName","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestCountry","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestLocation","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestRegion","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestWeather","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DistanceKilometers","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DistanceMiles","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightDelay","type":"boolean","esTypes":["boolean"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightDelayMin","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightDelayType","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightNum","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightTimeHour","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightTimeMin","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Origin","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginAirportID","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginCityName","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginCountry","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginLocation","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginRegion","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginWeather","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"_id","type":"string","esTypes":["_id"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_index","type":"string","esTypes":["_index"],"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","esTypes":["_source"],"count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_type","type":"string","esTypes":["_type"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"dayOfWeek","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"timestamp","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"hour_of_day":{"id":"number","params":{"pattern":"00"}},"AvgTicketPrice":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', }, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index f8d39e6689fa8..068ba66c4b0de 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -270,7 +270,7 @@ export const getSavedObjects = (): SavedObject[] => [ title: 'kibana_sample_data_logs', timeFieldName: 'timestamp', fields: - '[{"name":"@timestamp","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"_id","type":"string","esTypes":["_id"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_index","type":"string","esTypes":["_index"],"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","esTypes":["_source"],"count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_type","type":"string","esTypes":["_type"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"agent","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"agent.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "agent"}}},{"name":"bytes","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"clientip","type":"ip","esTypes":["ip"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"event.dataset","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"extension","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"extension.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "extension"}}},{"name":"geo.coordinates","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geo.dest","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geo.src","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geo.srcdest","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"host","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"host.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "host"}}},{"name":"index","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"index.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "index"}}},{"name":"ip","type":"ip","esTypes":["ip"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"machine.os","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"machine.os.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "machine.os"}}},{"name":"machine.ram","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"memory","type":"number","esTypes":["double"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"message","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"message.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "message"}}},{"name":"phpmemory","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"referer","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"request","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"request.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "request"}}},{"name":"response","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"response.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "response"}}},{"name":"tags","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"tags.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "tags"}}},{"name":"timestamp","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"url","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"url.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "url"}}},{"name":"utc_time","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"hour_of_day":{}}', }, references: [], From 68decb8352ce7fd2223dba140d7470d88867600b Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 1 Dec 2020 20:24:54 +0300 Subject: [PATCH 030/107] declare kbn/monaco dependency on kbn/i18n explicitly (#84660) --- packages/kbn-monaco/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index e2406a73f5342..eef68d3a35e0c 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -11,5 +11,8 @@ "devDependencies": { "@kbn/babel-preset": "link:../kbn-babel-preset", "@kbn/dev-utils": "link:../kbn-dev-utils" + }, + "dependencies": { + "@kbn/i18n": "link:../kbn-i18n" } } \ No newline at end of file From 6da6db28ac9738aab65581ca1faadc08642e511b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 1 Dec 2020 17:30:05 +0000 Subject: [PATCH 031/107] Revert the Revert of "[Alerting] renames Resolved action group to Recovered (#84123)" (#84662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reapplies the #84123 PR: This PR changes the default term from “Resolved” to “Recovered”, as it fits most use cases and we feel users are most likely to understand its meaning across domains. --- .../alerts/common/builtin_action_groups.ts | 10 +-- .../alerts/server/alert_type_registry.test.ts | 14 ++--- .../tests/get_alert_instance_summary.test.ts | 2 +- ...rt_instance_summary_from_event_log.test.ts | 62 ++++++++++++++++--- .../alert_instance_summary_from_event_log.ts | 5 +- x-pack/plugins/alerts/server/plugin.ts | 5 +- .../server/task_runner/task_runner.test.ts | 12 ++-- .../alerts/server/task_runner/task_runner.ts | 32 +++++----- .../inventory_metric_threshold_executor.ts | 4 +- .../metric_threshold_executor.test.ts | 6 +- .../metric_threshold_executor.ts | 4 +- .../public/application/constants/index.ts | 6 +- .../get_defaults_for_action_params.test.ts | 6 +- .../lib/get_defaults_for_action_params.ts | 4 +- .../action_form.test.tsx | 27 ++++---- .../action_type_form.tsx | 10 +-- .../tests/alerting/list_alert_types.ts | 4 +- .../spaces_only/tests/alerting/alerts_base.ts | 16 ++--- .../spaces_only/tests/alerting/event_log.ts | 14 ++--- .../alerting/get_alert_instance_summary.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- 21 files changed, 152 insertions(+), 95 deletions(-) diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index d31f75357d370..d9c5ae613f787 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,13 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const ResolvedActionGroup: ActionGroup = { - id: 'resolved', - name: i18n.translate('xpack.alerts.builtinActionGroups.resolved', { - defaultMessage: 'Resolved', +export const RecoveredActionGroup: ActionGroup = { + id: 'recovered', + name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { + defaultMessage: 'Recovered', }), }; export function getBuiltinActionGroups(): ActionGroup[] { - return [ResolvedActionGroup]; + return [RecoveredActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 8dc387f96addb..b04871a047e4b 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -105,8 +105,8 @@ describe('register()', () => { name: 'Default', }, { - id: 'resolved', - name: 'Resolved', + id: 'recovered', + name: 'Recovered', }, ], defaultActionGroupId: 'default', @@ -117,7 +117,7 @@ describe('register()', () => { expect(() => registry.register(alertType)).toThrowError( new Error( - `Alert type [id="${alertType.id}"] cannot be registered. Action groups [resolved] are reserved by the framework.` + `Alert type [id="${alertType.id}"] cannot be registered. Action groups [recovered] are reserved by the framework.` ) ); }); @@ -229,8 +229,8 @@ describe('get()', () => { "name": "Default", }, Object { - "id": "resolved", - "name": "Resolved", + "id": "recovered", + "name": "Recovered", }, ], "actionVariables": Object { @@ -287,8 +287,8 @@ describe('list()', () => { "name": "Test Action Group", }, Object { - "id": "resolved", - "name": "Resolved", + "id": "recovered", + "name": "Recovered", }, ], "actionVariables": Object { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 9bd61c0fe66d2..0a764ea768591 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -122,7 +122,7 @@ describe('getAlertInstanceSummary()', () => { .addActiveInstance('instance-previously-active', 'action group B') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-previously-active') + .addRecoveredInstance('instance-previously-active') .addActiveInstance('instance-currently-active', 'action group A') .getEvents(); const eventsResult = { diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index f9e4a2908d6ce..1d5ebe2b5911e 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary } from '../types'; import { IValidatedEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log'; const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; @@ -189,7 +189,43 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-1') + .addRecoveredInstance('instance-1') + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + }); + + test('legacy alert with currently inactive instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addLegacyResolvedInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -224,7 +260,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-1') + .addRecoveredInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -406,7 +442,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addResolvedInstance('instance-2') + .addRecoveredInstance('instance-2') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -451,7 +487,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addResolvedInstance('instance-2') + .addRecoveredInstance('instance-2') .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group B') @@ -561,12 +597,24 @@ export class EventsFactory { return this; } - addResolvedInstance(instanceId: string): EventsFactory { + addRecoveredInstance(instanceId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.recoveredInstance, + }, + kibana: { alerting: { instance_id: instanceId } }, + }); + return this; + } + + addLegacyResolvedInstance(instanceId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.resolvedInstance, + action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, }, kibana: { alerting: { instance_id: instanceId } }, }); diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index 8fed97a74435d..6fed8b4aa4ee6 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types'; import { IEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; export interface AlertInstanceSummaryFromEventLogParams { alert: SanitizedAlert; @@ -80,7 +80,8 @@ export function alertInstanceSummaryFromEventLog( status.status = 'Active'; status.actionGroupId = event?.kibana?.alerting?.action_group_id; break; - case EVENT_LOG_ACTIONS.resolvedInstance: + case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: + case EVENT_LOG_ACTIONS.recoveredInstance: status.status = 'OK'; status.activeStartDate = undefined; status.actionGroupId = undefined; diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 4bfb44425544a..bafb89c64076b 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -82,9 +82,12 @@ export const EVENT_LOG_ACTIONS = { execute: 'execute', executeAction: 'execute-action', newInstance: 'new-instance', - resolvedInstance: 'resolved-instance', + recoveredInstance: 'recovered-instance', activeInstance: 'active-instance', }; +export const LEGACY_EVENT_LOG_ACTIONS = { + resolvedInstance: 'resolved-instance', +}; export interface PluginSetupContract { registerType: AlertTypeRegistry['register']; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 07d08f5837d54..d4c4f746392c3 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -26,12 +26,12 @@ import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { Alert, ResolvedActionGroup } from '../../common'; +import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; const alertType = { id: 'test', name: 'My test alert', - actionGroups: [{ id: 'default', name: 'Default' }, ResolvedActionGroup], + actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], defaultActionGroupId: 'default', executor: jest.fn(), producer: 'alerts', @@ -114,7 +114,7 @@ describe('Task Runner', () => { }, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: '2', actionTypeId: 'action', params: { @@ -517,7 +517,7 @@ describe('Task Runner', () => { `); }); - test('fire resolved actions for execution for the alertInstances which is in the resolved state', async () => { + test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -650,7 +650,7 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "resolved-instance", + "action": "recovered-instance", }, "kibana": Object { "alerting": Object { @@ -666,7 +666,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' resolved instance: '2'", + "message": "test:1: 'alert-name' instance '2' has recovered", }, ], Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 24d96788c3395..5a7247ac50ea0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -39,7 +39,7 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; import { AlertsClient } from '../alerts_client'; import { partiallyUpdateAlert } from '../saved_objects'; -import { ResolvedActionGroup } from '../../common'; +import { RecoveredActionGroup } from '../../common'; const FALLBACK_RETRY_INTERVAL = '5m'; @@ -219,7 +219,7 @@ export class TaskRunner { alertInstance.hasScheduledActions() ); - generateNewAndResolvedInstanceEvents({ + generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, @@ -229,7 +229,7 @@ export class TaskRunner { }); if (!muteAll) { - scheduleActionsForResolvedInstances( + scheduleActionsForRecoveredInstances( alertInstances, executionHandler, originalAlertInstances, @@ -436,7 +436,7 @@ export class TaskRunner { } } -interface GenerateNewAndResolvedInstanceEventsParams { +interface GenerateNewAndRecoveredInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; @@ -445,18 +445,20 @@ interface GenerateNewAndResolvedInstanceEventsParams { namespace: string | undefined; } -function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { +function generateNewAndRecoveredInstanceEvents( + params: GenerateNewAndRecoveredInstanceEventsParams +) { const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); - const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); + const recoveredIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); - for (const id of resolvedIds) { + for (const id of recoveredIds) { const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; - const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); + const message = `${params.alertLabel} instance '${id}' has recovered`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup); } for (const id of newIds) { @@ -496,7 +498,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst } } -function scheduleActionsForResolvedInstances( +function scheduleActionsForRecoveredInstances( alertInstancesMap: Record, executionHandler: ReturnType, originalAlertInstances: Record, @@ -505,22 +507,22 @@ function scheduleActionsForResolvedInstances( ) { const currentAlertInstanceIds = Object.keys(currentAlertInstances); const originalAlertInstanceIds = Object.keys(originalAlertInstances); - const resolvedIds = without( + const recoveredIds = without( originalAlertInstanceIds, ...currentAlertInstanceIds, ...mutedInstanceIds ); - for (const id of resolvedIds) { + for (const id of recoveredIds) { const instance = alertInstancesMap[id]; - instance.updateLastScheduledActions(ResolvedActionGroup.id); + instance.updateLastScheduledActions(RecoveredActionGroup.id); instance.unscheduleActions(); executionHandler({ - actionGroup: ResolvedActionGroup.id, + actionGroup: RecoveredActionGroup.id, context: {}, state: {}, alertInstanceId: id, }); - instance.scheduleActions(ResolvedActionGroup.id); + instance.scheduleActions(RecoveredActionGroup.id); } } diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 14785f64cffac..1941ec6326ddb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; @@ -103,7 +103,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group: item, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index b31afba8ac9cc..a1d6428f3b52b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,7 +6,7 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, @@ -367,7 +367,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('does not continue to send a recovery alert if the metric is still OK', async () => { @@ -383,7 +383,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 7c3918c37ebbf..60790648d9a9b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraBackendLibs } from '../../infra_types'; import { @@ -89,7 +89,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const firstResult = first(alertResults); const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); const actionGroupId = - nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 7af8e5ba88300..156f65f094342 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -16,10 +16,10 @@ export const routeToConnectors = `/connectors`; export const routeToAlerts = `/alerts`; export const routeToAlertDetails = `/alert/:alertId`; -export const resolvedActionGroupMessage = i18n.translate( - 'xpack.triggersActionsUI.sections.actionForm.ResolvedMessage', +export const recoveredActionGroupMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.RecoveredMessage', { - defaultMessage: 'Resolved', + defaultMessage: 'Recovered', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts index c1f7cbb9fafed..57cc45786b2da 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResolvedActionGroup } from '../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../alerts/common'; import { AlertProvidedActionVariables } from './action_variables'; import { getDefaultsForActionParams } from './get_defaults_for_action_params'; @@ -16,8 +16,8 @@ describe('getDefaultsForActionParams', () => { }); }); - test('pagerduty defaults for resolved action group', async () => { - expect(getDefaultsForActionParams('.pagerduty', ResolvedActionGroup.id)).toEqual({ + test('pagerduty defaults for recovered action group', async () => { + expect(getDefaultsForActionParams('.pagerduty', RecoveredActionGroup.id)).toEqual({ dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, eventAction: 'resolve', }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts index c2143553e63c6..36c054977ac30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertActionParam, ResolvedActionGroup } from '../../../../alerts/common'; +import { AlertActionParam, RecoveredActionGroup } from '../../../../alerts/common'; import { AlertProvidedActionVariables } from './action_variables'; export const getDefaultsForActionParams = ( @@ -17,7 +17,7 @@ export const getDefaultsForActionParams = ( dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, eventAction: 'trigger', }; - if (actionGroupId === ResolvedActionGroup.id) { + if (actionGroupId === RecoveredActionGroup.id) { pagerDutyDefaults.eventAction = 'resolve'; } return pagerDutyDefaults; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 5b56720737b7e..ddbf933078043 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -10,8 +10,9 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { useKibana } from '../../../common/lib/kibana'; +import { EuiScreenReaderOnly } from '@elastic/eui'; jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -228,7 +229,7 @@ describe('action_form', () => { }} actionGroups={[ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; @@ -347,18 +348,18 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", - "inputDisplay": "Resolved", - "value": "resolved", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "inputDisplay": "Recovered", + "value": "recovered", }, ] `); }); - it('renders selected Resolved action group', async () => { + it('renders selected Recovered action group', async () => { const wrapper = await setup([ { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: 'test', actionTypeId: actionType.id, params: { @@ -381,15 +382,17 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", - "inputDisplay": "Resolved", - "value": "resolved", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "inputDisplay": "Recovered", + "value": "recovered", }, ] `); - expect(actionGroupsSelect.first().text()).toEqual( - 'Select an option: Resolved, is selectedResolved' + + expect(actionGroupsSelect.first().find(EuiScreenReaderOnly).text()).toEqual( + 'Select an option: Recovered, is selected' ); + expect(actionGroupsSelect.first().find('button').first().text()).toEqual('Recovered'); }); it('renders available connectors for the selected action type', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 7e38805957931..fffc3bd32125e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -26,7 +26,7 @@ import { EuiBadge, EuiErrorBoundary, } from '@elastic/eui'; -import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; +import { AlertActionParam, RecoveredActionGroup } from '../../../../../alerts/common'; import { IErrorObject, AlertAction, @@ -40,7 +40,7 @@ import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_en import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; import { transformActionVariables } from '../../lib/action_variables'; -import { resolvedActionGroupMessage } from '../../constants'; +import { recoveredActionGroupMessage } from '../../constants'; import { useKibana } from '../../../common/lib/kibana'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; @@ -105,8 +105,8 @@ export const ActionTypeForm = ({ useEffect(() => { setAvailableActionVariables(getAvailableActionVariables(messageVariables, actionItem.group)); const res = - actionItem.group === ResolvedActionGroup.id - ? resolvedActionGroupMessage + actionItem.group === RecoveredActionGroup.id + ? recoveredActionGroupMessage : defaultActionMessage; setAvailableDefaultActionMessage(res); const paramsDefaults = getDefaultsForActionParams(actionItem.actionTypeId, actionItem.group); @@ -374,7 +374,7 @@ function getAvailableActionVariables( return []; } const filteredActionVariables = - actionGroup === ResolvedActionGroup.id + actionGroup === RecoveredActionGroup.id ? { params: actionVariables.params, state: actionVariables.state } : actionVariables; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index b3635b9f40e27..bfaf8a2a4788e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -17,7 +17,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.noop', @@ -33,7 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedRestrictedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.restricted-noop', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 64e99190e183a..8dab199271da8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { ResolvedActionGroup } from '../../../../../plugins/alerts/common'; +import { RecoveredActionGroup } from '../../../../../plugins/alerts/common'; import { Space } from '../../../common/types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -137,7 +137,7 @@ instanceStateValue: true await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); }); - it('should fire actions when an alert instance is resolved', async () => { + it('should fire actions when an alert instance is recovered', async () => { const reference = alertUtils.generateReference(); const { body: createdAction } = await supertestWithoutAuth @@ -174,12 +174,12 @@ instanceStateValue: true params: {}, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Resolved message', + message: 'Recovered message', }, }, ], @@ -194,10 +194,10 @@ instanceStateValue: true await esTestIndexTool.waitForDocs('action:test.index-record', reference) )[0]; - expect(actionTestRecord._source.params.message).to.eql('Resolved message'); + expect(actionTestRecord._source.params.message).to.eql('Recovered message'); }); - it('should not fire actions when an alert instance is resolved, but alert is muted', async () => { + it('should not fire actions when an alert instance is recovered, but alert is muted', async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); @@ -237,12 +237,12 @@ instanceStateValue: true params: {}, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Resolved message', + message: 'Recovered message', }, }, ], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index d3cd3db124ecd..3766785680925 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -78,7 +78,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { 'execute-action', 'new-instance', 'active-instance', - 'resolved-instance', + 'recovered-instance', ], }); }); @@ -87,25 +87,25 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const executeEvents = getEventsByAction(events, 'execute'); const executeActionEvents = getEventsByAction(events, 'execute-action'); const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const resolvedInstanceEvents = getEventsByAction(events, 'resolved-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); expect(executeEvents.length >= 4).to.be(true); expect(executeActionEvents.length).to.be(2); expect(newInstanceEvents.length).to.be(1); - expect(resolvedInstanceEvents.length).to.be(1); + expect(recoveredInstanceEvents.length).to.be(1); // make sure the events are in the right temporal order const executeTimes = getTimestamps(executeEvents); const executeActionTimes = getTimestamps(executeActionEvents); const newInstanceTimes = getTimestamps(newInstanceEvents); - const resolvedInstanceTimes = getTimestamps(resolvedInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); // validate each event let executeCount = 0; @@ -136,8 +136,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { case 'new-instance': validateInstanceEvent(event, `created new instance: 'instance'`); break; - case 'resolved-instance': - validateInstanceEvent(event, `resolved instance: 'instance'`); + case 'recovered-instance': + validateInstanceEvent(event, `recovered instance: 'instance'`); break; case 'active-instance': validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 22034328e5275..404c6020fa237 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -216,7 +216,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr await alertUtils.muteInstance(createdAlert.id, 'instanceC'); await alertUtils.muteInstance(createdAlert.id, 'instanceD'); - await waitForEvents(createdAlert.id, ['new-instance', 'resolved-instance']); + await waitForEvents(createdAlert.id, ['new-instance', 'recovered-instance']); const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_instance_summary` ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 3fb2cc40437d8..9d38f4abb7f3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -25,7 +25,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType).to.eql({ actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.noop', From 9b74fe6d39b2d7126c650120a1c9b7523985f641 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 1 Dec 2020 12:55:15 -0500 Subject: [PATCH 032/107] [Fleet] Handler api key creation errors when Fleet Admin is invalid (#84576) --- .../components/new_enrollment_key_flyout.tsx | 3 + x-pack/plugins/fleet/server/errors/index.ts | 7 ++- .../server/services/api_keys/security.ts | 58 +++++++++++++++---- .../apis/enrollment_api_keys/crud.ts | 31 +++++++++- 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx index ed607e361bd6e..d9aeba2372672 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx @@ -46,6 +46,9 @@ function useCreateApiKeyForm( policy_id: policyIdInput.value, }), }); + if (res.error) { + throw res.error; + } policyIdInput.clear(); apiKeyNameInput.clear(); setIsLoading(false); diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index e3ca6a9b48dcf..d6fa79a2baeba 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -5,7 +5,11 @@ */ /* eslint-disable max-classes-per-file */ -export { defaultIngestErrorHandler, ingestErrorToResponseOptions } from './handlers'; +export { + defaultIngestErrorHandler, + ingestErrorToResponseOptions, + isLegacyESClientError, +} from './handlers'; export class IngestManagerError extends Error { constructor(message?: string) { @@ -24,3 +28,4 @@ export class PackageUnsupportedMediaTypeError extends IngestManagerError {} export class PackageInvalidArchiveError extends IngestManagerError {} export class PackageCacheError extends IngestManagerError {} export class PackageOperationNotSupportedError extends IngestManagerError {} +export class FleetAdminUserInvalidError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index dfd53d55fbbf5..5fdf8626a9fb2 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, FakeRequest, SavedObjectsClientContract } from 'src/core/server'; +import type { Request } from '@hapi/hapi'; +import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { FleetAdminUserInvalidError, isLegacyESClientError } from '../../errors'; import { CallESAsCurrentUser } from '../../types'; import { appContextService } from '../app_context'; import { outputService } from '../output'; @@ -18,22 +20,38 @@ export async function createAPIKey( if (!adminUser) { throw new Error('No admin user configured'); } - const request: FakeRequest = { + const request = KibanaRequest.from(({ + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, headers: { authorization: `Basic ${Buffer.from(`${adminUser.username}:${adminUser.password}`).toString( 'base64' )}`, }, - }; + } as unknown) as Request); const security = appContextService.getSecurity(); if (!security) { throw new Error('Missing security plugin'); } - return security.authc.createAPIKey(request as KibanaRequest, { - name, - role_descriptors: roleDescriptors, - }); + try { + const key = await security.authc.createAPIKey(request, { + name, + role_descriptors: roleDescriptors, + }); + + return key; + } catch (err) { + if (isLegacyESClientError(err) && err.statusCode === 401) { + // Clear Fleet admin user cache as the user is probably not valid anymore + outputService.invalidateCache(); + throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); + } + + throw err; + } } export async function authenticate(callCluster: CallESAsCurrentUser) { try { @@ -51,20 +69,36 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: if (!adminUser) { throw new Error('No admin user configured'); } - const request: FakeRequest = { + const request = KibanaRequest.from(({ + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, headers: { authorization: `Basic ${Buffer.from(`${adminUser.username}:${adminUser.password}`).toString( 'base64' )}`, }, - }; + } as unknown) as Request); const security = appContextService.getSecurity(); if (!security) { throw new Error('Missing security plugin'); } - return security.authc.invalidateAPIKey(request as KibanaRequest, { - id, - }); + try { + const res = await security.authc.invalidateAPIKey(request, { + id, + }); + + return res; + } catch (err) { + if (isLegacyESClientError(err) && err.statusCode === 401) { + // Clear Fleet admin user cache as the user is probably not valid anymore + outputService.invalidateCache(); + throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); + } + + throw err; + } } diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 3a526fac2f08a..dd72016476526 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -92,12 +92,12 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('should not allow to create an enrollment api key for a non existing agent policy', async () => { + it('should return a 400 if the fleet admin user is modifed outside of Fleet', async () => { await supertest .post(`/api/fleet/enrollment-api-keys`) .set('kbn-xsrf', 'xxx') .send({ - policy_id: 'idonotexistspolicy', + raoul: 'raoul', }) .expect(400); }); @@ -161,6 +161,33 @@ export default function (providerContext: FtrProviderContext) { }, }); }); + + describe('It should handle error when the Fleet user is invalid', () => { + before(async () => {}); + after(async () => { + await getService('supertest') + .post(`/api/fleet/agents/setup`) + .set('kbn-xsrf', 'xxx') + .send({ forceRecreate: true }); + }); + + it('should not allow to create an enrollment api key if the Fleet admin user is invalid', async () => { + await es.security.changePassword({ + username: 'fleet_enroll', + body: { + password: Buffer.from((Math.random() * 10000000).toString()).toString('base64'), + }, + }); + const res = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + }) + .expect(400); + expect(res.body.message).match(/Fleet Admin user is invalid/); + }); + }); }); }); } From d8643f719ecb449cc75b17f592ae76ae42e4f0d5 Mon Sep 17 00:00:00 2001 From: Jeffrey Chu <56368296+achuguy@users.noreply.github.com> Date: Tue, 1 Dec 2020 13:03:56 -0500 Subject: [PATCH 033/107] endpoint telemetry cloned endpoint tests (#81498) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data.json | 530 ++++ .../mappings.json | 2592 +++++++++++++++++ .../cloned_endpoint_installed/data.json | 533 ++++ .../cloned_endpoint_installed/mappings.json | 2592 +++++++++++++++++ .../cloned_endpoint_uninstalled/data.json | 527 ++++ .../cloned_endpoint_uninstalled/mappings.json | 2592 +++++++++++++++++ .../apps/endpoint/endpoint_telemetry.ts | 118 + 7 files changed, 9484 insertions(+) create mode 100644 x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json create mode 100644 x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json create mode 100644 x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json create mode 100644 x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json create mode 100644 x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json create mode 100644 x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json new file mode 100644 index 0000000000000..9b6804beabfe5 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json @@ -0,0 +1,530 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:37.093Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "MngOEjmt4OWFSCvya8AWgDF9p0nPqiCZLpNrqntWdjcGl+vPcbVs+un3ilKC3GQKtKP6KLtMziLR/60teHpAJ0Ls1f+mbCP1PjjAfFL1ZBnGHsvkR099iRJ9q4rCxzmZtifGZQ/s2+t99DRUe8GkJhIj3VR1uN/EKPXmXDWZo0f+bTUDT7vGZVY=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:58.866Z", + "default_api_key_id": "ieriwHQBXUUrssdI83FW", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "E-riwHQBXUUrssdIvHEw", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::4de:9ad6:320f:79f5/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.116/22", + "fdbb:cb5c:fb4:68:1cfe:7be7:f700:8810/64", + "fdbb:cb5c:fb4:68:257d:7303:389d:f335/64", + "fdbb:cb5c:fb4:68:7470:3bec:14b5:2caf/64", + "fdbb:cb5c:fb4:68:9c5f:eab7:8345:f711/64", + "fdbb:cb5c:fb4:68:dc96:8bac:67e0:99dd/64", + "fdbb:cb5c:fb4:68:60c6:73b6:1540:602/64", + "fdbb:cb5c:fb4:68:144:6a1b:1aae:a57d/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "mac": [ + "00:50:56:b1:7e:49" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:296c368b-35d3-4241-905f-75a24f52ec13", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "PEF8+bfiv21Yn5yj8I2/vIaQWMrUQK4PeBBwXsrvmVTsbuFejXM0IQtYVKXShBJAoY9CUEKPCRR4rIIdXWZc51i1ZneLoFw+yBw8BsSwhHfbQXvAVQowH7UqKHp0CiA5J9uGSgmw3Q55a4dv4IHih+sBKji7Qf2durs5gCWUJExrRCpMiU3OHSg=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:56.620Z", + "default_api_key_id": "xOrjwHQBXUUrssdIDnHH", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "S67iwHQBEiA0_Dvks-Cm", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.7.158/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:371f/64", + "fe80::250:56ff:feb1:371f/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "mac": [ + "00:50:56:b1:37:1f" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-38-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "296c368b-35d3-4241-905f-75a24f52ec13" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "sdv6996k+S1BiZ/12K3Wi6rb8Lsoh/+shwzKNqujwcmhdbeQ92ygLoO+tudJaJOnL129WT+hhanEf6OgH5PpQBezc03hl9v2AI+BlU+hssfce5OfgFRGLYg8S+ryNHwFhK6EJeN1aivoie+YholNpcpt2l/t+lQpevMI4QYGaMfUzofuivs5JM4=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:54.037Z", + "default_api_key_id": "lq7iwHQBEiA0_Dvk8-Fb", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "geriwHQBXUUrssdIqXB2", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::107d:2365:5a7c:8da/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.195/22", + "fdbb:cb5c:fb4:68:d4ef:63a5:8ffc:f933/64", + "fdbb:cb5c:fb4:68:b082:8681:cf85:27d0/64", + "fdbb:cb5c:fb4:68:7c3d:13f3:5339:be7b/64", + "fdbb:cb5c:fb4:68:19a4:2a63:cc88:6e59/64", + "fdbb:cb5c:fb4:68:494a:3867:57b8:4027/64", + "fdbb:cb5c:fb4:68:1c88:41e:6ce1:4be7/64", + "fdbb:cb5c:fb4:68:114:b84:8faf:b12b/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "mac": [ + "00:50:56:b1:e4:06" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:ac0ab6c1-2317-478c-93d9-c514d845302d", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "UnSz8pAKTP/0DENATzn13Yo0jcdbWq70IiBJcDY+DF5M063+El91o+448KVaMHj3rCSrULfJboBf1Ao80UKU5WKz4CYJ3ZVjHm39/f8rXMZSah5lQAkl9Ak2v5wUCFd4KTEwUUEmnUKKSQGC53cBhnvoyPdzfNjt1ml96lZFZbxXt/VyU3u8vhQ=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:30.880Z", + "default_api_key_id": "Va7iwHQBEiA0_DvkcN-4", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sq7iwHQBEiA0_DvkT98X", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-1", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:6c9d:def9:bb8a:6695/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-1", + "mac": [ + "00:50:56:b1:65:cb" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "ac0ab6c1-2317-478c-93d9-c514d845302d" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:2d187287-658a-4cb6-84d8-d66d1b9a6299", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "fpQcy/QWSbafzl6avELe9goTtyojPwQX3id1pe+BBqDarSCB3k5QwWLQP2SXEl2rwJdywUrBz3gMySKi80RYWJFUoWHiipfaE/jXJRqJxZZvhBe8fdSP7YPkdIdLQl/3ktIWqAzjjS1CErqMb5K4HTZIp5FswDQB40SbDkQKPECl9o8pBhLjH/A=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:28.949Z", + "default_api_key_id": "aeriwHQBXUUrssdIdXAX", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sa7iwHQBEiA0_DvkR99k", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-2", + "ip": [ + "fdbb:cb5c:fb4:68:dda8:b7a:3e20:9ca0/64", + "fdbb:cb5c:fb4:68:e922:9626:5193:ef68/128", + "fe80::dda8:b7a:3e20:9ca0/64", + "10.0.6.96/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-2", + "mac": [ + "00:50:56:b1:26:07" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "2d187287-658a-4cb6-84d8-d66d1b9a6299" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:c216aea0-58ba-40a3-b6fe-afa2f5457835", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "tSCsI7HPfRVIcw3Yx3xUAl20Hfe9AdEIs/4IBBH9ZO1gxnMMjRkVb/hxhfcdg6dkW+RIc6Pc9Jz7rUvybq8fY0r/pTKGXTFr46dC2+E9jfb7rs/PmYhG2V0/Ei2p+ZQypAIp8mtknSHkX+l74N7niVXKreneLrt99e4ZWIyeuwNwr0HcGjoMEqM=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:59.088Z", + "default_api_key_id": "SK7jwHQBEiA0_DvkNuIq", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "OeriwHQBXUUrssdIvXGr", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.6.176/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:3363/64", + "fe80::250:56ff:feb1:3363/64" + ], + "hostname": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "mac": [ + "00:50:56:b1:33:63" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-118-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "c216aea0-58ba-40a3-b6fe-afa2f5457835" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:8e652110-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:43.499165-04:00", + "subtype": "RUNNING", + "agent_id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "message": "Application: endpoint-security--7.9.2[5460518c-10c7-4c25-b2ec-3f63eafb7d47]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:43.495361445Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[44,4,0,2,2,4,1,2,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":25.33265565,\"mean\":6.21698140807909}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58376192,\"mean\":46094231}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.32258064516129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.323624595469256},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.664451827242525},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":9.55882352941176},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":308,\"system\":3807934}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"b364a499-8e64-4d91-9770-6911c5d6964b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"ec5403f8-6708-0d58-7aff-b2137b48b816\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:18:18.145Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:80a6c1f0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:54.930717796-04:00", + "subtype": "RUNNING", + "agent_id": "c216aea0-58ba-40a3-b6fe-afa2f5457835", + "message": "Application: endpoint-security--7.9.2[c216aea0-58ba-40a3-b6fe-afa2f5457835]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:54.929290223Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":3,\"mean\":3.49666666666667}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49778688,\"mean\":31986824}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":3863}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"a15f0431-6835-41c4-a7ee-21a70d41cf5b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"20ccfdfa-323f-e33e-f2ef-3528edb1afea\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:55.087Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7bdc8fb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:45.675453579-04:00", + "subtype": "RUNNING", + "agent_id": "296c368b-35d3-4241-905f-75a24f52ec13", + "message": "Application: endpoint-security--7.9.2[296c368b-35d3-4241-905f-75a24f52ec13]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:45.674010613Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":2.8,\"mean\":3.17}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49278976,\"mean\":31884356}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":5000305}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6f0cb2fc-3e46-4435-8892-d9f7e71b23fd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"e9909692-0e35-fd30-e3a3-e2e7253bb5c7\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:47.051Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:81e5aa90-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:40.138333-04:00", + "subtype": "RUNNING", + "agent_id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "message": "Application: endpoint-security--7.9.2[b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:40.134985503Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[55,0,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":10.21008368,\"mean\":1.91476589372881}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":71143424,\"mean\":53719456}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":3.08880308880309},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":302,\"system\":1901758}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"49f4e779-287a-4fa8-80e6-247b54c554f1\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"7d59b1a5-afa1-6531-07ea-691602558230\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:57.177Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:82b7eeb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:13.3157023-04:00", + "subtype": "RUNNING", + "agent_id": "ac0ab6c1-2317-478c-93d9-c514d845302d", + "message": "Application: endpoint-security--7.9.2[ac0ab6c1-2317-478c-93d9-c514d845302d]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:13.13714300Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[53,1,0,1,0,0,2,1,0,3,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":49.0526570938275,\"mean\":4.53577832211642}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":285802496,\"mean\":95647240}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":3.18021201413428},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":306,\"system\":3625}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6474b1bd-96bc-4bde-a770-0e6a7a5bf8c4\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"c85e6c40-d4a1-db21-7458-2565a6b857f3\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:58.555Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7cbf9cb1-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:15.400204-04:00", + "subtype": "RUNNING", + "agent_id": "2d187287-658a-4cb6-84d8-d66d1b9a6299", + "message": "Application: endpoint-security--7.9.2[2d187287-658a-4cb6-84d8-d66d1b9a6299]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:15.96990100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[46,2,2,2,4,2,0,0,0,2,0,0,0,0,1,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":48.3070275492921,\"mean\":6.43134047264261}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":228757504,\"mean\":94594836}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":1.9672131147541},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":2.62295081967213},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.655737704918033},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":2.11267605633803},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":307,\"system\":3654}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"21d182a2-5a08-41bb-b601-5d2b4aba4ecd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"327d0e20-483e-95af-f4e4-7b065606e1aa\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:48.539Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json new file mode 100644 index 0000000000000..27aea27bebcd7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json @@ -0,0 +1,2592 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "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" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "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" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "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" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "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" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json new file mode 100644 index 0000000000000..98488c85878b5 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json @@ -0,0 +1,533 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "sdv6996k+S1BiZ/12K3Wi6rb8Lsoh/+shwzKNqujwcmhdbeQ92ygLoO+tudJaJOnL129WT+hhanEf6OgH5PpQBezc03hl9v2AI+BlU+hssfce5OfgFRGLYg8S+ryNHwFhK6EJeN1aivoie+YholNpcpt2l/t+lQpevMI4QYGaMfUzofuivs5JM4=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:54.037Z", + "default_api_key_id": "lq7iwHQBEiA0_Dvk8-Fb", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "geriwHQBXUUrssdIqXB2", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::107d:2365:5a7c:8da/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.195/22", + "fdbb:cb5c:fb4:68:d4ef:63a5:8ffc:f933/64", + "fdbb:cb5c:fb4:68:b082:8681:cf85:27d0/64", + "fdbb:cb5c:fb4:68:7c3d:13f3:5339:be7b/64", + "fdbb:cb5c:fb4:68:19a4:2a63:cc88:6e59/64", + "fdbb:cb5c:fb4:68:494a:3867:57b8:4027/64", + "fdbb:cb5c:fb4:68:1c88:41e:6ce1:4be7/64", + "fdbb:cb5c:fb4:68:114:b84:8faf:b12b/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "mac": [ + "00:50:56:b1:e4:06" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:c216aea0-58ba-40a3-b6fe-afa2f5457835", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "tSCsI7HPfRVIcw3Yx3xUAl20Hfe9AdEIs/4IBBH9ZO1gxnMMjRkVb/hxhfcdg6dkW+RIc6Pc9Jz7rUvybq8fY0r/pTKGXTFr46dC2+E9jfb7rs/PmYhG2V0/Ei2p+ZQypAIp8mtknSHkX+l74N7niVXKreneLrt99e4ZWIyeuwNwr0HcGjoMEqM=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:59.088Z", + "default_api_key_id": "SK7jwHQBEiA0_DvkNuIq", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "OeriwHQBXUUrssdIvXGr", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.6.176/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:3363/64", + "fe80::250:56ff:feb1:3363/64" + ], + "hostname": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "mac": [ + "00:50:56:b1:33:63" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-118-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "c216aea0-58ba-40a3-b6fe-afa2f5457835" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:2d187287-658a-4cb6-84d8-d66d1b9a6299", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "fpQcy/QWSbafzl6avELe9goTtyojPwQX3id1pe+BBqDarSCB3k5QwWLQP2SXEl2rwJdywUrBz3gMySKi80RYWJFUoWHiipfaE/jXJRqJxZZvhBe8fdSP7YPkdIdLQl/3ktIWqAzjjS1CErqMb5K4HTZIp5FswDQB40SbDkQKPECl9o8pBhLjH/A=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:28.949Z", + "default_api_key_id": "aeriwHQBXUUrssdIdXAX", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sa7iwHQBEiA0_DvkR99k", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-2", + "ip": [ + "fdbb:cb5c:fb4:68:dda8:b7a:3e20:9ca0/64", + "fdbb:cb5c:fb4:68:e922:9626:5193:ef68/128", + "fe80::dda8:b7a:3e20:9ca0/64", + "10.0.6.96/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-2", + "mac": [ + "00:50:56:b1:26:07" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "2d187287-658a-4cb6-84d8-d66d1b9a6299" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:296c368b-35d3-4241-905f-75a24f52ec13", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "PEF8+bfiv21Yn5yj8I2/vIaQWMrUQK4PeBBwXsrvmVTsbuFejXM0IQtYVKXShBJAoY9CUEKPCRR4rIIdXWZc51i1ZneLoFw+yBw8BsSwhHfbQXvAVQowH7UqKHp0CiA5J9uGSgmw3Q55a4dv4IHih+sBKji7Qf2durs5gCWUJExrRCpMiU3OHSg=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:56.620Z", + "default_api_key_id": "xOrjwHQBXUUrssdIDnHH", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "S67iwHQBEiA0_Dvks-Cm", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.7.158/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:371f/64", + "fe80::250:56ff:feb1:371f/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "mac": [ + "00:50:56:b1:37:1f" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-38-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "296c368b-35d3-4241-905f-75a24f52ec13" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "MngOEjmt4OWFSCvya8AWgDF9p0nPqiCZLpNrqntWdjcGl+vPcbVs+un3ilKC3GQKtKP6KLtMziLR/60teHpAJ0Ls1f+mbCP1PjjAfFL1ZBnGHsvkR099iRJ9q4rCxzmZtifGZQ/s2+t99DRUe8GkJhIj3VR1uN/EKPXmXDWZo0f+bTUDT7vGZVY=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:58.866Z", + "default_api_key_id": "ieriwHQBXUUrssdI83FW", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "E-riwHQBXUUrssdIvHEw", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::4de:9ad6:320f:79f5/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.116/22", + "fdbb:cb5c:fb4:68:1cfe:7be7:f700:8810/64", + "fdbb:cb5c:fb4:68:257d:7303:389d:f335/64", + "fdbb:cb5c:fb4:68:7470:3bec:14b5:2caf/64", + "fdbb:cb5c:fb4:68:9c5f:eab7:8345:f711/64", + "fdbb:cb5c:fb4:68:dc96:8bac:67e0:99dd/64", + "fdbb:cb5c:fb4:68:60c6:73b6:1540:602/64", + "fdbb:cb5c:fb4:68:144:6a1b:1aae:a57d/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "mac": [ + "00:50:56:b1:7e:49" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:ac0ab6c1-2317-478c-93d9-c514d845302d", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "UnSz8pAKTP/0DENATzn13Yo0jcdbWq70IiBJcDY+DF5M063+El91o+448KVaMHj3rCSrULfJboBf1Ao80UKU5WKz4CYJ3ZVjHm39/f8rXMZSah5lQAkl9Ak2v5wUCFd4KTEwUUEmnUKKSQGC53cBhnvoyPdzfNjt1ml96lZFZbxXt/VyU3u8vhQ=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:30.880Z", + "default_api_key_id": "Va7iwHQBEiA0_DvkcN-4", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sq7iwHQBEiA0_DvkT98X", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-1", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:6c9d:def9:bb8a:6695/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-1", + "mac": [ + "00:50:56:b1:65:cb" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "ac0ab6c1-2317-478c-93d9-c514d845302d" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:81e5aa90-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:40.138333-04:00", + "subtype": "RUNNING", + "agent_id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "message": "Application: endpoint-security--7.9.2[b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:40.134985503Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[55,0,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":10.21008368,\"mean\":1.91476589372881}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":71143424,\"mean\":53719456}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":3.08880308880309},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":302,\"system\":1901758}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"49f4e779-287a-4fa8-80e6-247b54c554f1\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"7d59b1a5-afa1-6531-07ea-691602558230\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:57.177Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:8e652110-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:43.499165-04:00", + "subtype": "RUNNING", + "agent_id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "message": "Application: endpoint-security--7.9.2[5460518c-10c7-4c25-b2ec-3f63eafb7d47]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:43.495361445Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[44,4,0,2,2,4,1,2,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":25.33265565,\"mean\":6.21698140807909}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58376192,\"mean\":46094231}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.32258064516129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.323624595469256},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.664451827242525},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":9.55882352941176},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":308,\"system\":3807934}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"b364a499-8e64-4d91-9770-6911c5d6964b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"ec5403f8-6708-0d58-7aff-b2137b48b816\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:18:18.145Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:82b7eeb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:13.3157023-04:00", + "subtype": "RUNNING", + "agent_id": "ac0ab6c1-2317-478c-93d9-c514d845302d", + "message": "Application: endpoint-security--7.9.2[ac0ab6c1-2317-478c-93d9-c514d845302d]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:13.13714300Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[53,1,0,1,0,0,2,1,0,3,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":49.0526570938275,\"mean\":4.53577832211642}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":285802496,\"mean\":95647240}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":3.18021201413428},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":306,\"system\":3625}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6474b1bd-96bc-4bde-a770-0e6a7a5bf8c4\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"c85e6c40-d4a1-db21-7458-2565a6b857f3\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:58.555Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:80a6c1f0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:54.930717796-04:00", + "subtype": "RUNNING", + "agent_id": "c216aea0-58ba-40a3-b6fe-afa2f5457835", + "message": "Application: endpoint-security--7.9.2[c216aea0-58ba-40a3-b6fe-afa2f5457835]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:54.929290223Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":3,\"mean\":3.49666666666667}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49778688,\"mean\":31986824}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":3863}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"a15f0431-6835-41c4-a7ee-21a70d41cf5b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"20ccfdfa-323f-e33e-f2ef-3528edb1afea\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:55.087Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7bdc8fb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:45.675453579-04:00", + "subtype": "RUNNING", + "agent_id": "296c368b-35d3-4241-905f-75a24f52ec13", + "message": "Application: endpoint-security--7.9.2[296c368b-35d3-4241-905f-75a24f52ec13]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:45.674010613Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":2.8,\"mean\":3.17}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49278976,\"mean\":31884356}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":5000305}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6f0cb2fc-3e46-4435-8892-d9f7e71b23fd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"e9909692-0e35-fd30-e3a3-e2e7253bb5c7\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:47.051Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7cbf9cb1-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:15.400204-04:00", + "subtype": "RUNNING", + "agent_id": "2d187287-658a-4cb6-84d8-d66d1b9a6299", + "message": "Application: endpoint-security--7.9.2[2d187287-658a-4cb6-84d8-d66d1b9a6299]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:15.96990100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[46,2,2,2,4,2,0,0,0,2,0,0,0,0,1,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":48.3070275492921,\"mean\":6.43134047264261}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":228757504,\"mean\":94594836}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":1.9672131147541},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":2.62295081967213},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.655737704918033},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":2.11267605633803},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":307,\"system\":3654}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"21d182a2-5a08-41bb-b601-5d2b4aba4ecd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"327d0e20-483e-95af-f4e4-7b065606e1aa\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:48.539Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json new file mode 100644 index 0000000000000..27aea27bebcd7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json @@ -0,0 +1,2592 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "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" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "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" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "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" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "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" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json new file mode 100644 index 0000000000000..dbcd2604aed15 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json @@ -0,0 +1,527 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:38.636Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "MngOEjmt4OWFSCvya8AWgDF9p0nPqiCZLpNrqntWdjcGl+vPcbVs+un3ilKC3GQKtKP6KLtMziLR/60teHpAJ0Ls1f+mbCP1PjjAfFL1ZBnGHsvkR099iRJ9q4rCxzmZtifGZQ/s2+t99DRUe8GkJhIj3VR1uN/EKPXmXDWZo0f+bTUDT7vGZVY=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:58.866Z", + "default_api_key_id": "ieriwHQBXUUrssdI83FW", + "last_checkin": "2020-09-24T16:29:07.071Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "E-riwHQBXUUrssdIvHEw", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::4de:9ad6:320f:79f5/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.116/22", + "fdbb:cb5c:fb4:68:1cfe:7be7:f700:8810/64", + "fdbb:cb5c:fb4:68:257d:7303:389d:f335/64", + "fdbb:cb5c:fb4:68:7470:3bec:14b5:2caf/64", + "fdbb:cb5c:fb4:68:9c5f:eab7:8345:f711/64", + "fdbb:cb5c:fb4:68:dc96:8bac:67e0:99dd/64", + "fdbb:cb5c:fb4:68:60c6:73b6:1540:602/64", + "fdbb:cb5c:fb4:68:144:6a1b:1aae:a57d/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "mac": [ + "00:50:56:b1:7e:49" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:ac0ab6c1-2317-478c-93d9-c514d845302d", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.974Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "UnSz8pAKTP/0DENATzn13Yo0jcdbWq70IiBJcDY+DF5M063+El91o+448KVaMHj3rCSrULfJboBf1Ao80UKU5WKz4CYJ3ZVjHm39/f8rXMZSah5lQAkl9Ak2v5wUCFd4KTEwUUEmnUKKSQGC53cBhnvoyPdzfNjt1ml96lZFZbxXt/VyU3u8vhQ=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:30.880Z", + "default_api_key_id": "Va7iwHQBEiA0_DvkcN-4", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sq7iwHQBEiA0_DvkT98X", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-1", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:6c9d:def9:bb8a:6695/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-1", + "mac": [ + "00:50:56:b1:65:cb" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "ac0ab6c1-2317-478c-93d9-c514d845302d" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:c216aea0-58ba-40a3-b6fe-afa2f5457835", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.142Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "tSCsI7HPfRVIcw3Yx3xUAl20Hfe9AdEIs/4IBBH9ZO1gxnMMjRkVb/hxhfcdg6dkW+RIc6Pc9Jz7rUvybq8fY0r/pTKGXTFr46dC2+E9jfb7rs/PmYhG2V0/Ei2p+ZQypAIp8mtknSHkX+l74N7niVXKreneLrt99e4ZWIyeuwNwr0HcGjoMEqM=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:59.088Z", + "default_api_key_id": "SK7jwHQBEiA0_DvkNuIq", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "OeriwHQBXUUrssdIvXGr", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.6.176/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:3363/64", + "fe80::250:56ff:feb1:3363/64" + ], + "hostname": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "mac": [ + "00:50:56:b1:33:63" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-118-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "c216aea0-58ba-40a3-b6fe-afa2f5457835" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.072Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "sdv6996k+S1BiZ/12K3Wi6rb8Lsoh/+shwzKNqujwcmhdbeQ92ygLoO+tudJaJOnL129WT+hhanEf6OgH5PpQBezc03hl9v2AI+BlU+hssfce5OfgFRGLYg8S+ryNHwFhK6EJeN1aivoie+YholNpcpt2l/t+lQpevMI4QYGaMfUzofuivs5JM4=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:54.037Z", + "default_api_key_id": "lq7iwHQBEiA0_Dvk8-Fb", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "geriwHQBXUUrssdIqXB2", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::107d:2365:5a7c:8da/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.195/22", + "fdbb:cb5c:fb4:68:d4ef:63a5:8ffc:f933/64", + "fdbb:cb5c:fb4:68:b082:8681:cf85:27d0/64", + "fdbb:cb5c:fb4:68:7c3d:13f3:5339:be7b/64", + "fdbb:cb5c:fb4:68:19a4:2a63:cc88:6e59/64", + "fdbb:cb5c:fb4:68:494a:3867:57b8:4027/64", + "fdbb:cb5c:fb4:68:1c88:41e:6ce1:4be7/64", + "fdbb:cb5c:fb4:68:114:b84:8faf:b12b/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "mac": [ + "00:50:56:b1:e4:06" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:296c368b-35d3-4241-905f-75a24f52ec13", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.072Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "PEF8+bfiv21Yn5yj8I2/vIaQWMrUQK4PeBBwXsrvmVTsbuFejXM0IQtYVKXShBJAoY9CUEKPCRR4rIIdXWZc51i1ZneLoFw+yBw8BsSwhHfbQXvAVQowH7UqKHp0CiA5J9uGSgmw3Q55a4dv4IHih+sBKji7Qf2durs5gCWUJExrRCpMiU3OHSg=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:56.620Z", + "default_api_key_id": "xOrjwHQBXUUrssdIDnHH", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "S67iwHQBEiA0_Dvks-Cm", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.7.158/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:371f/64", + "fe80::250:56ff:feb1:371f/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "mac": [ + "00:50:56:b1:37:1f" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-38-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "296c368b-35d3-4241-905f-75a24f52ec13" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:2d187287-658a-4cb6-84d8-d66d1b9a6299", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.072Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "fpQcy/QWSbafzl6avELe9goTtyojPwQX3id1pe+BBqDarSCB3k5QwWLQP2SXEl2rwJdywUrBz3gMySKi80RYWJFUoWHiipfaE/jXJRqJxZZvhBe8fdSP7YPkdIdLQl/3ktIWqAzjjS1CErqMb5K4HTZIp5FswDQB40SbDkQKPECl9o8pBhLjH/A=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:28.949Z", + "default_api_key_id": "aeriwHQBXUUrssdIdXAX", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sa7iwHQBEiA0_DvkR99k", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-2", + "ip": [ + "fdbb:cb5c:fb4:68:dda8:b7a:3e20:9ca0/64", + "fdbb:cb5c:fb4:68:e922:9626:5193:ef68/128", + "fe80::dda8:b7a:3e20:9ca0/64", + "10.0.6.96/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-2", + "mac": [ + "00:50:56:b1:26:07" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "2d187287-658a-4cb6-84d8-d66d1b9a6299" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:81e5aa90-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:40.138333-04:00", + "subtype": "RUNNING", + "agent_id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "message": "Application: endpoint-security--7.9.2[b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:40.134985503Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[55,0,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":10.21008368,\"mean\":1.91476589372881}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":71143424,\"mean\":53719456}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":3.08880308880309},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":302,\"system\":1901758}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"49f4e779-287a-4fa8-80e6-247b54c554f1\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"7d59b1a5-afa1-6531-07ea-691602558230\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:57.177Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:8e652110-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:43.499165-04:00", + "subtype": "RUNNING", + "agent_id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "message": "Application: endpoint-security--7.9.2[5460518c-10c7-4c25-b2ec-3f63eafb7d47]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:43.495361445Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[44,4,0,2,2,4,1,2,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":25.33265565,\"mean\":6.21698140807909}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58376192,\"mean\":46094231}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.32258064516129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.323624595469256},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.664451827242525},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":9.55882352941176},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":308,\"system\":3807934}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"b364a499-8e64-4d91-9770-6911c5d6964b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"ec5403f8-6708-0d58-7aff-b2137b48b816\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:18:18.145Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:82b7eeb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:13.3157023-04:00", + "subtype": "RUNNING", + "agent_id": "ac0ab6c1-2317-478c-93d9-c514d845302d", + "message": "Application: endpoint-security--7.9.2[ac0ab6c1-2317-478c-93d9-c514d845302d]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:13.13714300Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[53,1,0,1,0,0,2,1,0,3,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":49.0526570938275,\"mean\":4.53577832211642}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":285802496,\"mean\":95647240}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":3.18021201413428},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":306,\"system\":3625}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6474b1bd-96bc-4bde-a770-0e6a7a5bf8c4\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"c85e6c40-d4a1-db21-7458-2565a6b857f3\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:58.555Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:80a6c1f0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:54.930717796-04:00", + "subtype": "RUNNING", + "agent_id": "c216aea0-58ba-40a3-b6fe-afa2f5457835", + "message": "Application: endpoint-security--7.9.2[c216aea0-58ba-40a3-b6fe-afa2f5457835]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:54.929290223Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":3,\"mean\":3.49666666666667}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49778688,\"mean\":31986824}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":3863}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"a15f0431-6835-41c4-a7ee-21a70d41cf5b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"20ccfdfa-323f-e33e-f2ef-3528edb1afea\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:55.087Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7bdc8fb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:45.675453579-04:00", + "subtype": "RUNNING", + "agent_id": "296c368b-35d3-4241-905f-75a24f52ec13", + "message": "Application: endpoint-security--7.9.2[296c368b-35d3-4241-905f-75a24f52ec13]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:45.674010613Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":2.8,\"mean\":3.17}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49278976,\"mean\":31884356}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":5000305}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6f0cb2fc-3e46-4435-8892-d9f7e71b23fd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"e9909692-0e35-fd30-e3a3-e2e7253bb5c7\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:47.051Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7cbf9cb1-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:15.400204-04:00", + "subtype": "RUNNING", + "agent_id": "2d187287-658a-4cb6-84d8-d66d1b9a6299", + "message": "Application: endpoint-security--7.9.2[2d187287-658a-4cb6-84d8-d66d1b9a6299]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:15.96990100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[46,2,2,2,4,2,0,0,0,2,0,0,0,0,1,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":48.3070275492921,\"mean\":6.43134047264261}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":228757504,\"mean\":94594836}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":1.9672131147541},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":2.62295081967213},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.655737704918033},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":2.11267605633803},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":307,\"system\":3654}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"21d182a2-5a08-41bb-b601-5d2b4aba4ecd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"327d0e20-483e-95af-f4e4-7b065606e1aa\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:48.539Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json new file mode 100644 index 0000000000000..27aea27bebcd7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json @@ -0,0 +1,2592 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "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" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "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" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "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" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "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" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts index 533ce49b14325..eb0cf4a34b2cc 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts @@ -157,5 +157,123 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + describe('when agents are connected with cloned endpoints', () => { + describe('with endpoint integration installed with malware enabled', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'cloned_endpoint_installed', + 'cloned_endpoint_test', + 0 + ); + await esArchiver.load('endpoint/telemetry/cloned_endpoint_test'); + await telemetryTestResources.deleteArchive('cloned_endpoint_test'); + }); + it('reports all endpoints and policies', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry).to.eql({ + total_installed: 6, + active_within_last_24_hours: 6, + os: [ + { + full_name: 'Ubuntu bionic(18.04.1 LTS (Bionic Beaver))', + platform: 'ubuntu', + version: '18.04.1 LTS (Bionic Beaver)', + count: 2, + }, + { + full_name: 'Mac OS X(10.14.1)', + platform: 'darwin', + version: '10.14.1', + count: 2, + }, + { + full_name: 'Windows 10 Pro(10.0)', + platform: 'windows', + version: '10.0', + count: 2, + }, + ], + policies: { + malware: { + active: 4, + inactive: 0, + failure: 0, + }, + }, + }); + }); + }); + describe('with endpoint integration installed on half the endpoints with malware enabled', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'cloned_endpoint_different_states', + 'cloned_endpoint_test', + 0 + ); + await esArchiver.load('endpoint/telemetry/cloned_endpoint_test'); + await telemetryTestResources.deleteArchive('cloned_endpoint_test'); + }); + it('reports all endpoints and policies', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry).to.eql({ + total_installed: 3, + active_within_last_24_hours: 3, + os: [ + { + full_name: 'Mac OS X(10.14.1)', + platform: 'darwin', + version: '10.14.1', + count: 1, + }, + { + full_name: 'Ubuntu bionic(18.04.1 LTS (Bionic Beaver))', + platform: 'ubuntu', + version: '18.04.1 LTS (Bionic Beaver)', + count: 1, + }, + { + full_name: 'Windows 10 Pro(10.0)', + platform: 'windows', + version: '10.0', + count: 1, + }, + ], + policies: { + malware: { + active: 2, + inactive: 0, + failure: 0, + }, + }, + }); + }); + }); + describe('with endpoint integration uninstalled', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'cloned_endpoint_uninstalled', + 'cloned_endpoint_test', + 0 + ); + await esArchiver.load('endpoint/telemetry/cloned_endpoint_test'); + await telemetryTestResources.deleteArchive('cloned_endpoint_test'); + }); + it('reports all endpoints and policies', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry).to.eql({ + total_installed: 0, + active_within_last_24_hours: 0, + os: [], + policies: { + malware: { + active: 0, + inactive: 0, + failure: 0, + }, + }, + }); + }); + }); + }); }); } From 5da9650f21337591cf4e62b8a218425b47abb6b2 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 1 Dec 2020 12:39:42 -0600 Subject: [PATCH 034/107] Changes UI links for drilldowns (#83971) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/core/public/doc_links/doc_links_service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6af14734444d1..15df6b34e22ff 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -39,9 +39,9 @@ export class DocLinksService { dashboard: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldown`, + drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldowns`, urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#variables`, + urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, From 78faa107aabc2b0ce6eed88f19633617244e19a1 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 1 Dec 2020 12:47:57 -0600 Subject: [PATCH 035/107] Add routes for use in Sources Schema (#84579) --- .../routes/workplace_search/sources.test.ts | 272 ++++++++++++++++++ .../server/routes/workplace_search/sources.ts | 172 +++++++++++ 2 files changed, 444 insertions(+) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 62f4dceeac363..d97a587e57ff2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -19,6 +19,9 @@ import { registerAccountPrepareSourcesRoute, registerAccountSourceSearchableRoute, registerAccountSourceDisplaySettingsConfig, + registerAccountSourceSchemasRoute, + registerAccountSourceReindexJobRoute, + registerAccountSourceReindexJobStatusRoute, registerOrgSourcesRoute, registerOrgSourcesStatusRoute, registerOrgSourceRoute, @@ -31,6 +34,9 @@ import { registerOrgPrepareSourcesRoute, registerOrgSourceSearchableRoute, registerOrgSourceDisplaySettingsConfig, + registerOrgSourceSchemasRoute, + registerOrgSourceReindexJobRoute, + registerOrgSourceReindexJobStatusRoute, registerOrgSourceOauthConfigurationsRoute, registerOrgSourceOauthConfigurationRoute, } from './sources'; @@ -523,6 +529,139 @@ describe('sources routes', () => { }); }); + describe('GET /api/workplace_search/account/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{id}/schemas', + payload: 'params', + }); + + registerAccountSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/schemas', + }); + }); + }); + + describe('POST /api/workplace_search/account/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/account/sources/{id}/schemas', + payload: 'body', + }); + + registerAccountSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: {}, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/schemas', + body: mockRequest.body, + }); + }); + }); + + describe('GET /api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}', + payload: 'params', + }); + + registerAccountSourceReindexJobRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/reindex_job/345', + }); + }); + }); + + describe('GET /api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}/status', + payload: 'params', + }); + + registerAccountSourceReindexJobStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/reindex_job/345/status', + }); + }); + }); + describe('GET /api/workplace_search/org/sources', () => { let mockRouter: MockRouter; @@ -1000,6 +1139,139 @@ describe('sources routes', () => { }); }); + describe('GET /api/workplace_search/org/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{id}/schemas', + payload: 'params', + }); + + registerOrgSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/schemas', + }); + }); + }); + + describe('POST /api/workplace_search/org/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/sources/{id}/schemas', + payload: 'body', + }); + + registerOrgSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: {}, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/schemas', + body: mockRequest.body, + }); + }); + }); + + describe('GET /api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}', + payload: 'params', + }); + + registerOrgSourceReindexJobRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/reindex_job/345', + }); + }); + }); + + describe('GET /api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}/status', + payload: 'params', + }); + + registerOrgSourceReindexJobStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/reindex_job/345/status', + }); + }); + }); + describe('GET /api/workplace_search/org/settings/connectors', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index d43a4252c7e1f..9beac109be510 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -339,6 +339,89 @@ export function registerAccountSourceDisplaySettingsConfig({ ); } +export function registerAccountSourceSchemasRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{id}/schemas', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/schemas`, + })(context, request, response); + } + ); + + router.post( + { + path: '/api/workplace_search/account/sources/{id}/schemas', + validate: { + body: schema.object({}), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/schemas`, + body: request.body, + })(context, request, response); + } + ); +} + +export function registerAccountSourceReindexJobRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.source_id}/reindex_job/${request.params.job_id}`, + })(context, request, response); + } + ); +} + +export function registerAccountSourceReindexJobStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}/status', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.source_id}/reindex_job/${request.params.job_id}/status`, + })(context, request, response); + } + ); +} + export function registerOrgSourcesRoute({ router, enterpriseSearchRequestHandler, @@ -638,6 +721,89 @@ export function registerOrgSourceDisplaySettingsConfig({ ); } +export function registerOrgSourceSchemasRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{id}/schemas', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/schemas`, + })(context, request, response); + } + ); + + router.post( + { + path: '/api/workplace_search/org/sources/{id}/schemas', + validate: { + body: schema.object({}), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/schemas`, + body: request.body, + })(context, request, response); + } + ); +} + +export function registerOrgSourceReindexJobRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.source_id}/reindex_job/${request.params.job_id}`, + })(context, request, response); + } + ); +} + +export function registerOrgSourceReindexJobStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}/status', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.source_id}/reindex_job/${request.params.job_id}/status`, + })(context, request, response); + } + ); +} + export function registerOrgSourceOauthConfigurationsRoute({ router, enterpriseSearchRequestHandler, @@ -741,6 +907,9 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerAccountPrepareSourcesRoute(dependencies); registerAccountSourceSearchableRoute(dependencies); registerAccountSourceDisplaySettingsConfig(dependencies); + registerAccountSourceSchemasRoute(dependencies); + registerAccountSourceReindexJobRoute(dependencies); + registerAccountSourceReindexJobStatusRoute(dependencies); registerOrgSourcesRoute(dependencies); registerOrgSourcesStatusRoute(dependencies); registerOrgSourceRoute(dependencies); @@ -753,6 +922,9 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerOrgPrepareSourcesRoute(dependencies); registerOrgSourceSearchableRoute(dependencies); registerOrgSourceDisplaySettingsConfig(dependencies); + registerOrgSourceSchemasRoute(dependencies); + registerOrgSourceReindexJobRoute(dependencies); + registerOrgSourceReindexJobStatusRoute(dependencies); registerOrgSourceOauthConfigurationsRoute(dependencies); registerOrgSourceOauthConfigurationRoute(dependencies); }; From 4b1ba0f3b261491f028c9b79d9163ff7c4000070 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 1 Dec 2020 12:10:14 -0700 Subject: [PATCH 036/107] [maps] remove fields from index-pattern test artifacts (#84379) * [maps] remove fields from index-pattern test artifacts * only remove non-scripted fields * fix data.json parse issues - darn trailing comma Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../es_archives/maps/kibana/data.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 71b4a85d63f08..5d6a355939d30 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -20,7 +20,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields" : "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"xss\"}}},{\"name\":\"hour_of_day\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['@timestamp'].value.getHour()\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", + "fields" : "[{\"name\":\"hour_of_day\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['@timestamp'].value.getHour()\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", "timeFieldName": "@timestamp", "title": "logstash-*" }, @@ -36,7 +36,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"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\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"prop1\",\"type\":\"number\",\"esTypes\":[\"byte\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "fields" : "[]", "title": "geo_shapes*" }, "type": "index-pattern" @@ -51,7 +51,7 @@ "index": ".kibana", "source": { "index-pattern": { - "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\":\"prop1\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"shape_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "fields" : "[]", "title": "meta_for_geo_shapes*" }, "type": "index-pattern" @@ -66,7 +66,7 @@ "index": ".kibana", "source": { "index-pattern": { - "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\":\"location\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "fields" : "[]", "title": "antimeridian_points" }, "type": "index-pattern" @@ -81,7 +81,7 @@ "index": ".kibana", "source": { "index-pattern": { - "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\":\"location\",\"type\":\"geo_shape\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", + "fields" : "[]", "title": "antimeridian_shapes" }, "type": "index-pattern" @@ -96,8 +96,8 @@ "index": ".kibana", "source": { "index-pattern" : { - "title" : "flights", - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"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\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"altitude\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"heading\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + "fields" : "[]", + "title" : "flights" }, "type" : "index-pattern", "references" : [ ], @@ -116,8 +116,8 @@ "index": ".kibana", "source": { "index-pattern" : { - "title" : "connections", - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"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\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"destination\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + "fields" : "[]", + "title" : "connections" }, "type" : "index-pattern", "references" : [ ], From ae9df6902863d78c422f93d260a216e2da8d0a9c Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 1 Dec 2020 14:24:02 -0600 Subject: [PATCH 037/107] [Enterprise Search] Migrate shared Indexing Status component (#84571) * Add react-motion package This is needed to animate the loading progress bar in the Enterprise Search schema views * Add shared interface * Migrate IndexingStatusContent component This is a straight copy/paste with only linting changes and tests added * Migrate IndexingStatusErrors component This is a copy/paste with linting changes and tests added. Also changed out the Link component to our EuiLinkTo component for internal routing * Migrate IndexingStatus component This is a straight copy/paste with only linting changes and tests added * Migrate IndexingStatusFetcher component This is a copy/paste with some modifications. The http/axios code has been removed in favor of the HTTPLogic in Kibana. This is a WIP that I am merging to master until I can get it working in the UI. Without either Schema component in the UIs for App Search or Workplace Search this is only a POC. Will not backport until this is verified working and have written tests. * Add i18n * Revert "Add react-motion package" This reverts commit 92db929d2ab45bba36be7fd53acee495e4969981. * Remove motion dependency * Update copy Co-authored-by: Constance * Refactor per code review - Remove stui classes - Inline status Co-authored-by: Constance --- .../shared/indexing_status/constants.ts | 28 ++++++++ .../shared/indexing_status/index.ts | 7 ++ .../indexing_status/indexing_status.test.tsx | 51 +++++++++++++++ .../indexing_status/indexing_status.tsx | 44 +++++++++++++ .../indexing_status_content.test.tsx | 21 ++++++ .../indexing_status_content.tsx | 27 ++++++++ .../indexing_status_errors.test.tsx | 25 ++++++++ .../indexing_status_errors.tsx | 31 +++++++++ .../indexing_status_fetcher.tsx | 64 +++++++++++++++++++ .../public/applications/shared/types.ts | 11 ++++ 10 files changed, 309 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts new file mode 100644 index 0000000000000..b2b76d5b987b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const INDEXING_STATUS_PROGRESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.progress.title', + { + defaultMessage: 'Indexing progress', + } +); + +export const INDEXING_STATUS_HAS_ERRORS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.hasErrors.title', + { + defaultMessage: 'Several documents have field conversion errors.', + } +); + +export const INDEXING_STATUS_HAS_ERRORS_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.hasErrors.button', + { + defaultMessage: 'View errors', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts new file mode 100644 index 0000000000000..4a97f11e8f0ee --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/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 { IndexingStatus } from './indexing_status'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx new file mode 100644 index 0000000000000..097c3bbc8e9ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiPanel } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; +import { IndexingStatusErrors } from './indexing_status_errors'; +import { IndexingStatusFetcher } from './indexing_status_fetcher'; +import { IndexingStatus } from './indexing_status'; + +describe('IndexingStatus', () => { + const getItemDetailPath = jest.fn(); + const getStatusPath = jest.fn(); + const onComplete = jest.fn(); + const setGlobalIndexingStatus = jest.fn(); + + const props = { + percentageComplete: 50, + numDocumentsWithErrors: 1, + activeReindexJobId: 12, + viewLinkPath: '/path', + itemId: '1', + getItemDetailPath, + getStatusPath, + onComplete, + setGlobalIndexingStatus, + }; + + it('renders', () => { + const wrapper = shallow(); + const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')( + props.percentageComplete, + props.numDocumentsWithErrors + ); + + expect(shallow(fetcher).find(EuiPanel)).toHaveLength(1); + expect(shallow(fetcher).find(IndexingStatusContent)).toHaveLength(1); + }); + + it('renders errors', () => { + const wrapper = shallow(); + const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')(100, 1); + expect(shallow(fetcher).find(IndexingStatusErrors)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx new file mode 100644 index 0000000000000..beec0babea590 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; +import { IndexingStatusErrors } from './indexing_status_errors'; +import { IndexingStatusFetcher } from './indexing_status_fetcher'; + +import { IIndexingStatus } from '../types'; + +export interface IIndexingStatusProps extends IIndexingStatus { + viewLinkPath: string; + itemId: string; + getItemDetailPath?(itemId: string): string; + getStatusPath(itemId: string, activeReindexJobId: number): string; + onComplete(numDocumentsWithErrors: number): void; + setGlobalIndexingStatus?(activeReindexJob: IIndexingStatus): void; +} + +export const IndexingStatus: React.FC = (props) => ( + + {(percentageComplete, numDocumentsWithErrors) => ( +
+ {percentageComplete < 100 && ( + + + + )} + {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( + <> + + + + )} +
+ )} +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx new file mode 100644 index 0000000000000..9fe0e890e6943 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiProgress, EuiTitle } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; + +describe('IndexingStatusContent', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTitle)).toHaveLength(1); + expect(wrapper.find(EuiProgress)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx new file mode 100644 index 0000000000000..a0c67388621a8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiProgress, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { INDEXING_STATUS_PROGRESS_TITLE } from './constants'; + +interface IIndexingStatusContentProps { + percentageComplete: number; +} + +export const IndexingStatusContent: React.FC = ({ + percentageComplete, +}) => ( +
+ +

{INDEXING_STATUS_PROGRESS_TITLE}

+
+ + +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx new file mode 100644 index 0000000000000..fc706aee659a5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiCallOut, EuiButton } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { IndexingStatusErrors } from './indexing_status_errors'; + +describe('IndexingStatusErrors', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButton)).toHaveLength(1); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo).prop('to')).toEqual('/path'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx new file mode 100644 index 0000000000000..a928400b2338c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiButton, EuiCallOut } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { INDEXING_STATUS_HAS_ERRORS_TITLE, INDEXING_STATUS_HAS_ERRORS_BUTTON } from './constants'; + +interface IIndexingStatusErrorsProps { + viewLinkPath: string; +} + +export const IndexingStatusErrors: React.FC = ({ viewLinkPath }) => ( + +

{INDEXING_STATUS_HAS_ERRORS_TITLE}

+ + {INDEXING_STATUS_HAS_ERRORS_BUTTON} + +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx new file mode 100644 index 0000000000000..cb7c82f91ed61 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, useRef } from 'react'; + +import { HttpLogic } from '../http'; +import { flashAPIErrors } from '../flash_messages'; + +interface IIndexingStatusFetcherProps { + activeReindexJobId: number; + itemId: string; + percentageComplete: number; + numDocumentsWithErrors: number; + onComplete?(numDocumentsWithErrors: number): void; + getStatusPath(itemId: string, activeReindexJobId: number): string; + children(percentageComplete: number, numDocumentsWithErrors: number): JSX.Element; +} + +export const IndexingStatusFetcher: React.FC = ({ + activeReindexJobId, + children, + getStatusPath, + itemId, + numDocumentsWithErrors, + onComplete, + percentageComplete = 0, +}) => { + const [indexingStatus, setIndexingStatus] = useState({ + numDocumentsWithErrors, + percentageComplete, + }); + const pollingInterval = useRef(); + + useEffect(() => { + pollingInterval.current = window.setInterval(async () => { + try { + const response = await HttpLogic.values.http.get(getStatusPath(itemId, activeReindexJobId)); + if (response.percentageComplete >= 100) { + clearInterval(pollingInterval.current); + } + setIndexingStatus({ + percentageComplete: response.percentageComplete, + numDocumentsWithErrors: response.numDocumentsWithErrors, + }); + if (response.percentageComplete >= 100 && onComplete) { + onComplete(response.numDocumentsWithErrors); + } + } catch (e) { + flashAPIErrors(e); + } + }, 3000); + + return () => { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + }; + }, []); + + return children(indexingStatus.percentageComplete, indexingStatus.numDocumentsWithErrors); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts new file mode 100644 index 0000000000000..3866d1a7199e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IIndexingStatus { + percentageComplete: number; + numDocumentsWithErrors: number; + activeReindexJobId: number; +} From 4ae84a00a549ed834d618d19e00008666791f107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 1 Dec 2020 21:37:56 +0100 Subject: [PATCH 038/107] [CCR] Fix row actions in follower index and auto-follow pattern tables (#84433) * Fixed auto follow actions * Created a provider for all follower index table actions to fix modal auto-closing * Moved i18n texts into a const to avoid duplication * Removed unnecessary imports and added index.js file for follower_index_actions_provider imports * Fixed wrong imports deletion * Fixed wrong imports deletion Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../follower_index_actions_provider.js | 34 ++++ .../follower_index_pause_provider.js | 6 +- .../follower_index_resume_provider.js | 8 +- .../follower_index_unfollow_provider.js | 4 +- .../follower_index_actions_providers/index.js | 10 + .../public/app/components/index.js | 7 +- .../auto_follow_pattern_table.js | 178 ++++++++--------- .../follower_indices_table.js | 185 ++++++++---------- 8 files changed, 217 insertions(+), 215 deletions(-) create mode 100644 x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js rename x-pack/plugins/cross_cluster_replication/public/app/components/{ => follower_index_actions_providers}/follower_index_pause_provider.js (95%) rename x-pack/plugins/cross_cluster_replication/public/app/components/{ => follower_index_actions_providers}/follower_index_resume_provider.js (95%) rename x-pack/plugins/cross_cluster_replication/public/app/components/{ => follower_index_actions_providers}/follower_index_unfollow_provider.js (97%) create mode 100644 x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js new file mode 100644 index 0000000000000..df3017ebf92a4 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { FollowerIndexPauseProvider } from './follower_index_pause_provider'; +import { FollowerIndexResumeProvider } from './follower_index_resume_provider'; +import { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider'; + +export const FollowerIndexActionsProvider = (props) => { + return ( + + {(pauseFollowerIndex) => ( + + {(resumeFollowerIndex) => ( + + {(unfollowLeaderIndex) => { + const { children } = props; + return children(() => ({ + pauseFollowerIndex, + resumeFollowerIndex, + unfollowLeaderIndex, + })); + }} + + )} + + )} + + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js similarity index 95% rename from x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js index 9c1e8255d069c..7d1168d831631 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js @@ -11,9 +11,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { pauseFollowerIndex } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; -import { areAllSettingsDefault } from '../services/follower_index_default_settings'; +import { pauseFollowerIndex } from '../../store/actions'; +import { arrify } from '../../../../common/services/utils'; +import { areAllSettingsDefault } from '../../services/follower_index_default_settings'; class FollowerIndexPauseProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js similarity index 95% rename from x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js index 101e3df6bf710..86f8c0447e734 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js @@ -10,10 +10,10 @@ import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui'; -import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public'; -import { routing } from '../services/routing'; -import { resumeFollowerIndex } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { routing } from '../../services/routing'; +import { resumeFollowerIndex } from '../../store/actions'; +import { arrify } from '../../../../common/services/utils'; class FollowerIndexResumeProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js similarity index 97% rename from x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js index 68b6b970ad90b..f9644aa20c2c2 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js @@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { unfollowLeaderIndex } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; +import { unfollowLeaderIndex } from '../../store/actions'; +import { arrify } from '../../../../common/services/utils'; class FollowerIndexUnfollowProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js new file mode 100644 index 0000000000000..fe1a7d82a56a1 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FollowerIndexActionsProvider } from './follower_index_actions_provider'; +export { FollowerIndexPauseProvider } from './follower_index_pause_provider'; +export { FollowerIndexResumeProvider } from './follower_index_resume_provider'; +export { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/components/index.js index 6d971bff03981..55609fa85fb11 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/index.js @@ -12,9 +12,10 @@ export { AutoFollowPatternForm } from './auto_follow_pattern_form'; export { AutoFollowPatternDeleteProvider } from './auto_follow_pattern_delete_provider'; export { AutoFollowPatternPageTitle } from './auto_follow_pattern_page_title'; export { AutoFollowPatternIndicesPreview } from './auto_follow_pattern_indices_preview'; -export { FollowerIndexPauseProvider } from './follower_index_pause_provider'; -export { FollowerIndexResumeProvider } from './follower_index_resume_provider'; -export { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider'; +export { FollowerIndexPauseProvider } from './follower_index_actions_providers'; +export { FollowerIndexResumeProvider } from './follower_index_actions_providers'; +export { FollowerIndexUnfollowProvider } from './follower_index_actions_providers'; +export { FollowerIndexActionsProvider } from './follower_index_actions_providers'; export { FollowerIndexForm } from './follower_index_form'; export { FollowerIndexPageTitle } from './follower_index_page_title'; export { FormEntryRow } from './form_entry_row'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 2309dece3f92b..dd5fe6f212808 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { @@ -13,7 +13,6 @@ import { EuiLoadingKibana, EuiOverlayMask, EuiHealth, - EuiIcon, } from '@elastic/eui'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK } from '../../../../../constants'; import { @@ -23,6 +22,33 @@ import { import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; +const actionI18nTexts = { + pause: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionPauseDescription', + { + defaultMessage: 'Pause replication', + } + ), + resume: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionResumeDescription', + { + defaultMessage: 'Resume replication', + } + ), + edit: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionEditDescription', + { + defaultMessage: 'Edit auto-follow pattern', + } + ), + delete: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionDeleteDescription', + { + defaultMessage: 'Delete auto-follow pattern', + } + ), +}; + const getFilteredPatterns = (autoFollowPatterns, queryText) => { if (queryText) { const normalizedSearchText = queryText.toLowerCase(); @@ -93,7 +119,7 @@ export class AutoFollowPatternTable extends PureComponent { }); }; - getTableColumns() { + getTableColumns(deleteAutoFollowPattern) { const { selectAutoFollowPattern } = this.props; return [ @@ -200,88 +226,34 @@ export class AutoFollowPatternTable extends PureComponent { ), actions: [ { - render: ({ name, active }) => { - const label = active - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionPauseDescription', - { - defaultMessage: 'Pause replication', - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionResumeDescription', - { - defaultMessage: 'Resume replication', - } - ); - - return ( - { - if (event.stopPropagation) { - event.stopPropagation(); - } - if (active) { - this.props.pauseAutoFollowPattern(name); - } else { - this.props.resumeAutoFollowPattern(name); - } - }} - data-test-subj={active ? 'contextMenuPauseButton' : 'contextMenuResumeButton'} - > - - {label} - - ); - }, + name: actionI18nTexts.pause, + description: actionI18nTexts.pause, + icon: 'pause', + onClick: (item) => this.props.pauseAutoFollowPattern(item.name), + available: (item) => item.active, + 'data-test-subj': 'contextMenuPauseButton', }, { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionEditDescription', - { - defaultMessage: 'Edit auto-follow pattern', - } - ); - - return ( - routing.navigate(routing.getAutoFollowPatternPath(name))} - data-test-subj="contextMenuEditButton" - > - - {label} - - ); - }, + name: actionI18nTexts.resume, + description: actionI18nTexts.resume, + icon: 'play', + onClick: (item) => this.props.resumeAutoFollowPattern(item.name), + available: (item) => !item.active, + 'data-test-subj': 'contextMenuResumeButton', }, { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionDeleteDescription', - { - defaultMessage: 'Delete auto-follow pattern', - } - ); - - return ( - - {(deleteAutoFollowPattern) => ( - deleteAutoFollowPattern(name)} - data-test-subj="contextMenuDeleteButton" - > - - {label} - - )} - - ); - }, + name: actionI18nTexts.edit, + description: actionI18nTexts.edit, + icon: 'pencil', + onClick: (item) => routing.navigate(routing.getAutoFollowPatternPath(item.name)), + 'data-test-subj': 'contextMenuEditButton', + }, + { + name: actionI18nTexts.delete, + description: actionI18nTexts.delete, + icon: 'trash', + onClick: (item) => deleteAutoFollowPattern(item.name), + 'data-test-subj': 'contextMenuDeleteButton', }, ], width: '100px', @@ -339,26 +311,30 @@ export class AutoFollowPatternTable extends PureComponent { }; return ( - - ({ - 'data-test-subj': 'row', - })} - cellProps={(item, column) => ({ - 'data-test-subj': `cell_${column.field}`, - })} - data-test-subj="autoFollowPatternListTable" - /> - {this.renderLoading()} - + + {(deleteAutoFollowPattern) => ( + <> + ({ + 'data-test-subj': 'row', + })} + cellProps={(item, column) => ({ + 'data-test-subj': `cell_${column.field}`, + })} + data-test-subj="autoFollowPatternListTable" + /> + {this.renderLoading()} + + )} + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 0c57b3f7330cf..2ea73e272b24e 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiIcon, EuiHealth, EuiInMemoryTable, EuiLink, @@ -17,15 +16,38 @@ import { EuiOverlayMask, } from '@elastic/eui'; import { API_STATUS, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK } from '../../../../../constants'; -import { - FollowerIndexPauseProvider, - FollowerIndexResumeProvider, - FollowerIndexUnfollowProvider, -} from '../../../../../components'; +import { FollowerIndexActionsProvider } from '../../../../../components'; import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; +const actionI18nTexts = { + pause: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionPauseDescription', + { + defaultMessage: 'Pause replication', + } + ), + resume: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionResumeDescription', + { + defaultMessage: 'Resume replication', + } + ), + edit: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionEditDescription', + { + defaultMessage: 'Edit follower index', + } + ), + unfollow: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionUnfollowDescription', + { + defaultMessage: 'Unfollow leader index', + } + ), +}; + const getFilteredIndices = (followerIndices, queryText) => { if (queryText) { const normalizedSearchText = queryText.toLowerCase(); @@ -101,91 +123,43 @@ export class FollowerIndicesTable extends PureComponent { routing.navigate(uri); }; - getTableColumns() { + getTableColumns(actionHandlers) { const { selectFollowerIndex } = this.props; const actions = [ - /* Pause or resume follower index */ + /* Pause follower index */ { - render: (followerIndex) => { - const { name, isPaused } = followerIndex; - const label = isPaused - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionResumeDescription', - { - defaultMessage: 'Resume replication', - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionPauseDescription', - { - defaultMessage: 'Pause replication', - } - ); - - return isPaused ? ( - - {(resumeFollowerIndex) => ( - resumeFollowerIndex(name)} data-test-subj="resumeButton"> - - {label} - - )} - - ) : ( - - {(pauseFollowerIndex) => ( - pauseFollowerIndex(followerIndex)} - data-test-subj="pauseButton" - > - - {label} - - )} - - ); - }, + name: actionI18nTexts.pause, + description: actionI18nTexts.pause, + icon: 'pause', + onClick: (item) => actionHandlers.pauseFollowerIndex(item), + available: (item) => !item.isPaused, + 'data-test-subj': 'pauseButton', + }, + /* Resume follower index */ + { + name: actionI18nTexts.resume, + description: actionI18nTexts.resume, + icon: 'play', + onClick: (item) => actionHandlers.resumeFollowerIndex(item.name), + available: (item) => item.isPaused, + 'data-test-subj': 'resumeButton', }, /* Edit follower index */ { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionEditDescription', - { - defaultMessage: 'Edit follower index', - } - ); - - return ( - this.editFollowerIndex(name)} data-test-subj="editButton"> - - {label} - - ); - }, + name: actionI18nTexts.edit, + description: actionI18nTexts.edit, + onClick: (item) => this.editFollowerIndex(item.name), + icon: 'pencil', + 'data-test-subj': 'editButton', }, /* Unfollow leader index */ { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionUnfollowDescription', - { - defaultMessage: 'Unfollow leader index', - } - ); - - return ( - - {(unfollowLeaderIndex) => ( - unfollowLeaderIndex(name)} data-test-subj="unfollowButton"> - - {label} - - )} - - ); - }, + name: actionI18nTexts.unfollow, + description: actionI18nTexts.unfollow, + onClick: (item) => actionHandlers.unfollowLeaderIndex(item.name), + icon: 'indexFlush', + 'data-test-subj': 'unfollowButton', }, ]; @@ -321,26 +295,33 @@ export class FollowerIndicesTable extends PureComponent { }; return ( - - ({ - 'data-test-subj': 'row', - })} - cellProps={(item, column) => ({ - 'data-test-subj': `cell-${column.field}`, - })} - data-test-subj="followerIndexListTable" - /> - {this.renderLoading()} - + + {(getActionHandlers) => { + const actionHandlers = getActionHandlers(); + return ( + <> + ({ + 'data-test-subj': 'row', + })} + cellProps={(item, column) => ({ + 'data-test-subj': `cell-${column.field}`, + })} + data-test-subj="followerIndexListTable" + /> + {this.renderLoading()} + + ); + }} + ); } } From 49f0ca082756673d9613bdf576023e7a00a305ed Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 1 Dec 2020 16:33:16 -0500 Subject: [PATCH 039/107] [Maps] Support runtime fields in tooltips (#84377) --- .../classes/sources/es_search_source/es_search_source.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index d31f6ee626245..1c0645ae797ec 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -487,8 +487,14 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return {}; } + const { docValueFields } = getDocValueAndSourceFields( + indexPattern, + this._getTooltipPropertyNames() + ); + + const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source const searchService = getSearchService(); - const searchSource = searchService.searchSource.createEmpty(); + const searchSource = await searchService.searchSource.create(initialSearchContext as object); searchSource.setField('index', indexPattern); searchSource.setField('size', 1); From b9a64ba7d5f859e4803f38544a471040111a4100 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Dec 2020 23:39:27 +0200 Subject: [PATCH 040/107] [Security Solutino][Case] Case connector alert UI (#82405) Co-authored-by: Patryk Kopycinski Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/common/constants.ts | 6 + .../components/add_comment/index.test.tsx | 94 ++-- .../cases/components/add_comment/index.tsx | 18 +- .../all_cases/table_filters.test.tsx | 1 - .../configure_cases/connectors_dropdown.tsx | 2 +- .../configure_cases/field_mapping.test.tsx | 3 +- .../configure_cases/field_mapping.tsx | 14 +- .../configure_cases/field_mapping_row.tsx | 2 +- .../components/configure_cases/index.tsx | 2 +- .../connector_selector/form.test.tsx | 68 +++ .../components/connector_selector/form.tsx | 24 +- .../connectors/case/cases_dropdown.tsx | 72 +++ .../connectors/case/existing_case.tsx | 54 +++ .../components/connectors/case/fields.tsx | 101 ++++ .../components}/connectors/case/index.ts | 22 +- .../connectors/case/translations.ts | 86 ++++ .../cases/components/connectors/case/types.ts | 17 + .../components}/connectors/config.ts | 0 .../components}/connectors/index.ts | 4 + .../components}/connectors/types.ts | 0 .../components}/connectors/utils.ts | 0 .../components/create/connector.test.tsx | 186 ++++++++ .../cases/components/create/connector.tsx | 83 ++++ .../components/create/description.test.tsx | 57 +++ .../cases/components/create/description.tsx | 31 ++ .../cases/components/create/form.test.tsx | 66 +++ .../public/cases/components/create/form.tsx | 94 ++++ .../cases/components/create/form_context.tsx | 66 +++ .../cases/components/create/index.test.tsx | 443 +++++++++++++----- .../public/cases/components/create/index.tsx | 342 ++------------ .../optional_field_label/index.test.tsx | 18 + .../create/optional_field_label/index.tsx | 2 +- .../public/cases/components/create/schema.tsx | 7 +- .../components/create/submit_button.test.tsx | 83 ++++ .../cases/components/create/submit_button.tsx | 30 ++ .../cases/components/create/tags.test.tsx | 77 +++ .../public/cases/components/create/tags.tsx | 48 ++ .../cases/components/create/title.test.tsx | 67 +++ .../public/cases/components/create/title.tsx | 32 ++ .../public/cases/components/settings/card.tsx | 2 +- .../cases/components/settings/fields_form.tsx | 23 +- .../cases/components/tag_list/index.tsx | 5 +- .../use_all_cases_modal/all_cases_modal.tsx | 5 +- .../create_case_modal.tsx | 68 +++ .../use_create_case_modal/index.tsx | 45 ++ .../components/use_insert_timeline/index.tsx | 63 +++ .../lib/connectors/case/translations.ts | 21 - .../security_solution/public/plugin.tsx | 2 +- .../use_insert_timeline.tsx | 70 --- 49 files changed, 2018 insertions(+), 608 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connector_selector/form.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx rename x-pack/plugins/security_solution/public/{common/lib => cases/components}/connectors/case/index.ts (57%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/case/types.ts rename x-pack/plugins/security_solution/public/{common/lib => cases/components}/connectors/config.ts (100%) rename x-pack/plugins/security_solution/public/{common/lib => cases/components}/connectors/index.ts (79%) rename x-pack/plugins/security_solution/public/{common/lib => cases/components}/connectors/types.ts (100%) rename x-pack/plugins/security_solution/public/{common/lib => cases/components}/connectors/utils.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/connector.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/description.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/form.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/tags.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/title.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/title.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e58aed15a8a10..c47ec70341845 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -160,6 +160,7 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID]; /* Rule notifications options */ +export const ENABLE_CASE_CONNECTOR = false; export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', @@ -169,6 +170,11 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.jira', '.resilient', ]; + +if (ENABLE_CASE_CONNECTOR) { + NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.case'); +} + export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; export const NOTIFICATION_THROTTLE_RULE = 'rule'; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 2c8051f902b17..50e139bcd215f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -6,42 +6,24 @@ import React from 'react'; import { mount } from 'enzyme'; +import { waitFor, act } from '@testing-library/react'; import { AddComment, AddCommentRefObject } from '.'; import { TestProviders } from '../../../common/mock'; -import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; import { CommentRequest, CommentType } from '../../../../../case/common/api'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import { useInsertTimeline } from '../use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; -import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; -import { waitFor } from '@testing-library/react'; - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' -); - -jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_comment'); +jest.mock('../use_insert_timeline'); -const useFormMock = useForm as jest.Mock; -const useFormDataMock = useFormData as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; const usePostCommentMock = usePostComment as jest.Mock; - +const useInsertTimelineMock = useInsertTimeline as jest.Mock; const onCommentSaving = jest.fn(); const onCommentPosted = jest.fn(); const postComment = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); const addCommentProps = { caseId: '1234', @@ -52,15 +34,6 @@ const addCommentProps = { showLoading: false, }; -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; - const defaultPostCommment = { isLoading: false, isError: false, @@ -73,14 +46,9 @@ const sampleData: CommentRequest = { }; describe('AddComment ', () => { - const formHookMock = getFormMock(sampleData); - beforeEach(() => { jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCommentMock.mockImplementation(() => defaultPostCommment); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - useFormDataMock.mockImplementation(() => [{ comment: sampleData.comment }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); }); @@ -92,14 +60,25 @@ describe('AddComment ', () => { ); + + await act(async () => { + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.comment } }); + }); + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); - wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click'); + await act(async () => { + wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click'); + }); + await waitFor(() => { expect(onCommentSaving).toBeCalled(); expect(postComment).toBeCalledWith(sampleData, onCommentPosted); - expect(formHookMock.reset).toBeCalled(); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(''); }); }); @@ -112,6 +91,7 @@ describe('AddComment ', () => { ); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); expect( wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled') @@ -127,15 +107,16 @@ describe('AddComment ', () => { ); + expect( wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled') ).toBeTruthy(); }); - it('should insert a quote', () => { + it('should insert a quote', async () => { const sampleQuote = 'what a cool quote'; const ref = React.createRef(); - mount( + const wrapper = mount( @@ -143,10 +124,37 @@ describe('AddComment ', () => { ); - ref.current!.addQuote(sampleQuote); - expect(formHookMock.setFieldValue).toBeCalledWith( - 'comment', + await act(async () => { + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.comment } }); + }); + + await act(async () => { + ref.current!.addQuote(sampleQuote); + }); + + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe( `${sampleData.comment}\n\n${sampleQuote}` ); }); + + it('it should insert a timeline', async () => { + useInsertTimelineMock.mockImplementation((comment, onTimelineAttached) => { + onTimelineAttached(`[title](url)`); + }); + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 859ba3d1a0951..daa7c24858b94 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -12,12 +12,11 @@ import { CommentType } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; import * as i18n from './translations'; import { schema, AddCommentFormSchema } from './schema'; -import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click'; +import { useInsertTimeline } from '../use_insert_timeline'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -56,12 +55,6 @@ export const AddComment = React.memo( const { setFieldValue, reset, submit } = form; const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] }); - const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [ - setFieldValue, - ]); - - const { handleCursorChange } = useInsertTimeline(comment, onCommentChange); - const addQuote = useCallback( (quote) => { setFieldValue(fieldName, `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); @@ -73,7 +66,12 @@ export const AddComment = React.memo( addQuote, })); - const handleTimelineClick = useTimelineClick(); + const onTimelineAttached = useCallback( + (newValue: string) => setFieldValue(fieldName, newValue), + [setFieldValue] + ); + + useInsertTimeline(comment ?? '', onTimelineAttached); const onSubmit = useCallback(async () => { const { isValid, data } = await submit(); @@ -98,8 +96,6 @@ export const AddComment = React.memo( isDisabled: isLoading, dataTestSubj: 'add-comment', placeholder: i18n.ADD_COMMENT_HELP_TEXT, - onCursorPositionUpdate: handleCursorChange, - onClickTimeline: handleTimelineClick, bottomRightContent: ( { + const formHookMock = getFormMock({ connectorId: connectorsMock[0].id }); + + beforeEach(() => { + jest.resetAllMocks(); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + }); + + it('it should render', async () => { + const wrapper = mount( +
+ + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + }); + + it('it should not render when is not in edit mode', async () => { + const wrapper = mount( +
+ + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index 7de7b3d6b2a96..9017365eea02b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; @@ -14,9 +15,8 @@ import { ActionConnector } from '../../../../../case/common/api/cases'; interface ConnectorSelectorProps { connectors: ActionConnector[]; dataTestSubj: string; - defaultValue?: ActionConnector; disabled: boolean; - field: FieldHook; + field: FieldHook; idAria: string; isEdit: boolean; isLoading: boolean; @@ -24,7 +24,6 @@ interface ConnectorSelectorProps { export const ConnectorSelector = ({ connectors, dataTestSubj, - defaultValue, disabled = false, field, idAria, @@ -32,19 +31,6 @@ export const ConnectorSelector = ({ isLoading = false, }: ConnectorSelectorProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - useEffect(() => { - field.setValue(defaultValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValue]); - - const handleContentChange = useCallback( - (newConnector: string) => { - field.setValue(newConnector); - }, - [field] - ); - return isEdit ? ( ) : null; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx new file mode 100644 index 0000000000000..931e23e811b1b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; +import React, { memo, useMemo, useCallback } from 'react'; +import { Case } from '../../../containers/types'; + +import * as i18n from './translations'; + +interface CaseDropdownProps { + isLoading: boolean; + cases: Case[]; + selectedCase?: string; + onCaseChanged: (id: string) => void; +} + +export const ADD_CASE_BUTTON_ID = 'add-case'; + +const addNewCase = { + value: ADD_CASE_BUTTON_ID, + inputDisplay: ( + + {i18n.CASE_CONNECTOR_ADD_NEW_CASE} + + ), + 'data-test-subj': 'dropdown-connector-add-connector', +}; + +const CasesDropdownComponent: React.FC = ({ + isLoading, + cases, + selectedCase, + onCaseChanged, +}) => { + const caseOptions: Array> = useMemo( + () => + cases.reduce>>( + (acc, theCase) => [ + ...acc, + { + value: theCase.id, + inputDisplay: {theCase.title}, + 'data-test-subj': `case-connector-cases-dropdown-${theCase.id}`, + }, + ], + [] + ), + [cases] + ); + + const options = useMemo(() => [...caseOptions, addNewCase], [caseOptions]); + const onChange = useCallback((id: string) => onCaseChanged(id), [onCaseChanged]); + + return ( + + + + ); +}; + +export const CasesDropdown = memo(CasesDropdownComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx new file mode 100644 index 0000000000000..28e051a713bf4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo, useCallback } from 'react'; +import { useGetCases } from '../../../containers/use_get_cases'; +import { useCreateCaseModal } from '../../use_create_case_modal'; +import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown'; + +interface ExistingCaseProps { + selectedCase: string | null; + onCaseChanged: (id: string) => void; +} + +const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { + const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(); + + const onCaseCreated = useCallback(() => refetchCases(), [refetchCases]); + + const { Modal: CreateCaseModal, openModal } = useCreateCaseModal({ onCaseCreated }); + + const onChange = useCallback( + (id: string) => { + if (id === ADD_CASE_BUTTON_ID) { + openModal(); + return; + } + + onCaseChanged(id); + }, + [onCaseChanged, openModal] + ); + + const isCasesLoading = useMemo( + () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'), + [isLoadingCases] + ); + + return ( + <> + + + + ); +}; + +export const ExistingCase = memo(ExistingCaseComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx new file mode 100644 index 0000000000000..91087138e52d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { EuiCallOut } from '@elastic/eui'; + +import { ActionParamsProps } from '../../../../../../triggers_actions_ui/public/types'; +import { CommentType } from '../../../../../../case/common/api'; + +import { CaseActionParams } from './types'; +import { ExistingCase } from './existing_case'; + +import * as i18n from './translations'; + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui?.euiSize ?? '16px'}; + `} +`; + +const defaultAlertComment = { + type: CommentType.alert, + alertId: '{{context.rule.id}}', + index: '{{context.rule.output_index}}', +}; + +const CaseParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + actionConnector, +}) => { + const { caseId = null, comment = defaultAlertComment } = actionParams.subActionParams ?? {}; + + const [selectedCase, setSelectedCase] = useState(null); + + const editSubActionProperty = useCallback( + (key: string, value: unknown) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }, + [actionParams.subActionParams, editAction, index] + ); + + const onCaseChanged = useCallback( + (id: string) => { + setSelectedCase(id); + editSubActionProperty('caseId', id); + }, + [editSubActionProperty] + ); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'addComment', index); + } + + if (!actionParams.subActionParams?.caseId) { + editSubActionProperty('caseId', caseId); + } + + if (!actionParams.subActionParams?.comment) { + editSubActionProperty('comment', comment); + } + + if (caseId != null) { + setSelectedCase((prevCaseId) => (prevCaseId !== caseId ? caseId : prevCaseId)); + } + + // editAction creates an infinity loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + actionConnector, + index, + actionParams.subActionParams?.caseId, + actionParams.subActionParams?.comment, + caseId, + comment, + actionParams.subAction, + ]); + + return ( + <> + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { CaseParamsFields as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/index.ts similarity index 57% rename from x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/case/index.ts index 271b1bfd2e3de..0aacd61991771 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/index.ts @@ -3,11 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { lazy } from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ActionTypeModel } from '../../../../../../triggers_actions_ui/public/types'; +import { CaseActionParams } from './types'; import * as i18n from './translations'; +interface ValidationResult { + errors: { + caseId: string[]; + }; +} + +const validateParams = (actionParams: CaseActionParams) => { + const validationResult: ValidationResult = { errors: { caseId: [] } }; + + if (actionParams.subActionParams && !actionParams.subActionParams.caseId) { + validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED); + } + + return validationResult; +}; + export function getActionType(): ActionTypeModel { return { id: '.case', @@ -15,8 +33,8 @@ export function getActionType(): ActionTypeModel { selectMessage: i18n.CASE_CONNECTOR_DESC, actionTypeTitle: i18n.CASE_CONNECTOR_TITLE, validateConnector: () => ({ errors: {} }), - validateParams: () => ({ errors: {} }), + validateParams, actionConnectorFields: null, - actionParamsFields: null, + actionParamsFields: lazy(() => import('./fields')), }; } diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts new file mode 100644 index 0000000000000..8cfcf2b9a073b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../../translations'; + +export const CASE_CONNECTOR_DESC = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.selectMessageText', + { + defaultMessage: 'Create or update a case.', + } +); + +export const CASE_CONNECTOR_TITLE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.actionTypeTitle', + { + defaultMessage: 'Cases', + } +); + +export const CASE_CONNECTOR_COMMENT_LABEL = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.commentLabel', + { + defaultMessage: 'Comment', + } +); + +export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.commentRequired', + { + defaultMessage: 'Comment is required.', + } +); + +export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel', + { + defaultMessage: 'Case', + } +); + +export const CASE_CONNECTOR_CASES_DROPDOWN_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder', + { + defaultMessage: 'Select case', + } +); + +export const CASE_CONNECTOR_CASES_OPTION_NEW_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.optionAddNewCase', + { + defaultMessage: 'Add to a new case', + } +); + +export const CASE_CONNECTOR_CASES_OPTION_EXISTING_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase', + { + defaultMessage: 'Add to existing case', + } +); + +export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.caseRequired', + { + defaultMessage: 'You must select a case.', + } +); + +export const CASE_CONNECTOR_CALL_OUT_INFO = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.callOutInfo', + { + defaultMessage: 'All alerts after rule creation will be appended to the selected case.', + } +); + +export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.addNewCaseOption', + { + defaultMessage: 'Add new case', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/types.ts new file mode 100644 index 0000000000000..8173a814c2d89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface CaseActionParams { + subAction: string; + subActionParams: { + caseId: string; + comment: { + alertId: string; + index: string; + type: 'alert'; + }; + }; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/config.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/config.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts similarity index 79% rename from x-pack/plugins/security_solution/public/common/lib/connectors/index.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/index.ts index 58d7e89e080e7..e77aa9bdd84b1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts @@ -5,3 +5,7 @@ */ export { getActionType as getCaseConnectorUI } from './case'; + +export * from './config'; +export * from './types'; +export * from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/types.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/types.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx new file mode 100644 index 0000000000000..89b9e3b30ede1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { useForm, Form, FormHook } from '../../../shared_imports'; +import { connectorsMock } from '../../containers/mock'; +import { Connector } from './connector'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; +import { useGetSeverity } from '../settings/resilient/use_get_severity'; + +jest.mock('../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + notifications: {}, + http: {}, + }, + }), + }; +}); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../settings/resilient/use_get_incident_types'); +jest.mock('../settings/resilient/use_get_severity'); + +const useConnectorsMock = useConnectors as jest.Mock; +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; + +const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], +}; + +const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], +}; + +describe('Connector', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ connectorId: string; fields: Record | null }>({ + defaultValue: { connectorId: connectorsMock[0].id, fields: null }, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + }); + + it('it renders', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-settings"]`).exists()).toBeTruthy(); + + waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); + }); + }); + + it('it is loading when fetching connectors', async () => { + useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock }); + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + }); + + it('it is disabled when fetching connectors', async () => { + useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock }); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual( + true + ); + }); + + it('it is disabled and loading when passing loading as true', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual( + true + ); + }); + + it(`it should change connector`, async () => { + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeTruthy(); + }); + + act(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + act(() => { + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + }); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ + connectorId: 'resilient-2', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx new file mode 100644 index 0000000000000..b2a0f3c351552 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; +import { UseField, useFormData, FieldHook } from '../../../shared_imports'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { ConnectorSelector } from '../connector_selector/form'; +import { SettingFieldsForm } from '../settings/fields_form'; +import { ActionConnector } from '../../containers/types'; +import { getConnectorById } from '../configure_cases/utils'; + +interface Props { + isLoading: boolean; +} + +interface SettingsFieldProps { + connectors: ActionConnector[]; + field: FieldHook; + isEdit: boolean; +} + +const SettingsField = ({ connectors, isEdit, field }: SettingsFieldProps) => { + const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); + const { setValue } = field; + const connector = getConnectorById(connectorId, connectors) ?? null; + + useEffect(() => { + if (connectorId) { + setValue(null); + } + }, [setValue, connectorId]); + + return ( + + ); +}; + +const ConnectorComponent: React.FC = ({ isLoading }) => { + const { loading: isLoadingConnectors, connectors } = useConnectors(); + + return ( + + + + + + + + + ); +}; + +ConnectorComponent.displayName = 'ConnectorComponent'; + +export const Connector = memo(ConnectorComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx new file mode 100644 index 0000000000000..201a61febc628 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { act } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../../shared_imports'; +import { Description } from './description'; + +describe('Description', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ description: string }>({ + defaultValue: { description: 'My description' }, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy(); + }); + + it('it changes the description', async () => { + const wrapper = mount( + + + + ); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: 'My new description' } }); + }); + + expect(globalForm.getFormData()).toEqual({ description: 'My new description' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/description.tsx b/x-pack/plugins/security_solution/public/cases/components/create/description.tsx new file mode 100644 index 0000000000000..f130bd14644f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/description.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; +import { UseField } from '../../../shared_imports'; + +interface Props { + isLoading: boolean; +} + +export const fieldName = 'description'; + +const DescriptionComponent: React.FC = ({ isLoading }) => ( + +); + +DescriptionComponent.displayName = 'DescriptionComponent'; + +export const Description = memo(DescriptionComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx new file mode 100644 index 0000000000000..e64b2b3a05080 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { useForm, Form } from '../../../shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { connectorsMock } from '../../containers/mock'; +import { schema, FormProps } from './schema'; +import { CreateCaseForm } from './form'; + +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/configure/use_connectors'); +const useGetTagsMock = useGetTags as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; + +const initialCaseValue: FormProps = { + description: '', + tags: [], + title: '', + connectorId: 'none', + fields: null, +}; + +describe('CreateCaseForm', () => { + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + }); + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useGetTagsMock.mockReturnValue({ tags: ['test'] }); + useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + }); + + it('it renders with steps', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeTruthy(); + }); + + it('it renders without steps', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx new file mode 100644 index 0000000000000..40db4d792c1c8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx @@ -0,0 +1,94 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiLoadingSpinner, EuiSteps } from '@elastic/eui'; +import styled, { css } from 'styled-components'; + +import { useFormContext } from '../../../shared_imports'; + +import { Title } from './title'; +import { Description } from './description'; +import { Tags } from './tags'; +import { Connector } from './connector'; +import * as i18n from './translations'; + +interface ContainerProps { + big?: boolean; +} + +const Container = styled.div.attrs((props) => props)` + ${({ big, theme }) => css` + margin-top: ${big ? theme.eui?.euiSizeXL ?? '32px' : theme.eui?.euiSize ?? '16px'}; + `} +`; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; + z-index: 99; +`; + +interface Props { + withSteps?: boolean; +} + +export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) => { + const { isSubmitting } = useFormContext(); + + const firstStep = useMemo( + () => ({ + title: i18n.STEP_ONE_TITLE, + children: ( + <> + + <Container> + <Tags isLoading={isSubmitting} /> + </Container> + <Container big> + <Description isLoading={isSubmitting} /> + </Container> + </> + ), + }), + [isSubmitting] + ); + + const secondStep = useMemo( + () => ({ + title: i18n.STEP_TWO_TITLE, + children: ( + <Container> + <Connector isLoading={isSubmitting} /> + </Container> + ), + }), + [isSubmitting] + ); + + const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]); + + return ( + <> + {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + {withSteps ? ( + <EuiSteps + headingElement="h2" + steps={allSteps} + data-test-subj={'case-creation-form-steps'} + /> + ) : ( + <> + {firstStep.children} + {secondStep.children} + </> + )} + </> + ); +}); + +CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx new file mode 100644 index 0000000000000..e11e508b60ebf --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -0,0 +1,66 @@ +/* + * 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, { useCallback, useEffect } from 'react'; + +import { schema, FormProps } from './schema'; +import { Form, useForm } from '../../../shared_imports'; +import { + getConnectorById, + getNoneConnector, + normalizeActionConnector, +} from '../configure_cases/utils'; +import { usePostCase } from '../../containers/use_post_case'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { Case } from '../../containers/types'; + +const initialCaseValue: FormProps = { + description: '', + tags: [], + title: '', + connectorId: 'none', + fields: null, +}; + +interface Props { + onSuccess?: (theCase: Case) => void; +} + +export const FormContext: React.FC<Props> = ({ children, onSuccess }) => { + const { connectors } = useConnectors(); + const { caseData, postCase } = usePostCase(); + + const submitCase = useCallback( + async ({ connectorId: dataConnectorId, fields, ...dataWithoutConnectorId }, isValid) => { + if (isValid) { + const caseConnector = getConnectorById(dataConnectorId, connectors); + const connectorToUpdate = caseConnector + ? normalizeActionConnector(caseConnector, fields) + : getNoneConnector(); + + await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate }); + } + }, + [postCase, connectors] + ); + + const { form } = useForm<FormProps>({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + onSubmit: submitCase, + }); + + useEffect(() => { + if (caseData && onSuccess) { + onSuccess(caseData); + } + }, [caseData, onSuccess]); + + return <Form form={form}>{children}</Form>; +}; + +FormContext.displayName = 'FormContext'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 7902c7065d9a3..29073e7774158 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -5,71 +5,40 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { Create } from '.'; +import { mount, ReactWrapper } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { TestProviders } from '../../../common/mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostCase } from '../../containers/use_post_case'; import { useGetTags } from '../../containers/use_get_tags'; - -import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; - -import { waitFor } from '@testing-library/react'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; +import { useGetSeverity } from '../settings/resilient/use_get_severity'; +import { useGetIssueTypes } from '../settings/jira/use_get_issue_types'; +import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type'; +import { Create } from '.'; -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - // eslint-disable-next-line react/display-name - EuiFieldText: () => <input />, - }; -}); -jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_case'); - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' -); - jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', - () => ({ - FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => - children({ tags: ['rad', 'dude'] }), - }) -); -const useConnectorsMock = useConnectors as jest.Mock; -const useFormMock = useForm as jest.Mock; -const useFormDataMock = useFormData as jest.Mock; +jest.mock('../settings/resilient/use_get_incident_types'); +jest.mock('../settings/resilient/use_get_severity'); +jest.mock('../settings/jira/use_get_issue_types'); +jest.mock('../settings/jira/use_get_fields_by_issue_type'); +jest.mock('../settings/jira/use_get_single_issue'); +jest.mock('../settings/jira/use_get_issues'); -const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; const usePostCaseMock = usePostCase as jest.Mock; - +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; const postCase = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; const sampleTags = ['coke', 'pepsi']; const sampleData = { @@ -83,27 +52,117 @@ const sampleData = { type: ConnectorTypes.none, }, }; + const defaultPostCase = { isLoading: false, isError: false, caseData: null, postCase, }; + const sampleConnectorData = { loading: false, connectors: [] }; + +const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], +}; + +const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], +}; + +const useGetIssueTypesResponse = { + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], +}; + +const useGetFieldsByIssueTypeResponse = { + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '2', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, +}; + +const fillForm = async (wrapper: ReactWrapper) => { + await act(async () => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: sampleData.title } }); + }); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.description } }); + }); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(sampleTags.map((tag) => ({ label: tag }))); + }); +}; + describe('Create case', () => { const fetchTags = jest.fn(); - const formHookMock = getFormMock(sampleData); beforeEach(() => { jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCaseMock.mockImplementation(() => defaultPostCase); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - useFormDataMock.mockImplementation(() => [ - { - description: sampleData.description, - }, - ]); useConnectorsMock.mockReturnValue(sampleConnectorData); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); (useGetTags as jest.Mock).mockImplementation(() => ({ tags: sampleTags, @@ -112,7 +171,32 @@ describe('Create case', () => { }); describe('Step 1 - Case Fields', () => { + it('it renders', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).first().exists()).toBeTruthy(); + expect( + wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists() + ).toBeTruthy(); + }); + it('should post case on submit click', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -120,7 +204,13 @@ describe('Create case', () => { </Router> </TestProviders> ); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await fillForm(wrapper); + wrapper.update(); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); await waitFor(() => expect(postCase).toBeCalledWith(sampleData)); }); @@ -132,15 +222,18 @@ describe('Create case', () => { </Router> </TestProviders> ); + wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/')); }); + it('should redirect to new case when caseData is there', async () => { - const sampleId = '777777'; + const sampleId = 'case-id'; usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId }, })); + mount( <TestProviders> <Router history={mockHistory}> @@ -148,11 +241,11 @@ describe('Create case', () => { </Router> </TestProviders> ); - await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/777777')); + + await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/case-id')); }); it('should render spinner when loading', async () => { - usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -160,11 +253,87 @@ describe('Create case', () => { </Router> </TestProviders> ); + + await fillForm(wrapper); + await act(async () => { + await wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + wrapper.update(); + expect( + wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists() + ).toBeTruthy(); + }); + }); + }); + + describe('Step 2 - Connector Fields', () => { + it(`it should submit a Jira connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + + await fillForm(wrapper); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy(); + }); + + act(() => { + wrapper + .find('select[data-test-subj="issueTypeSelect"]') + .first() + .simulate('change', { + target: { value: '10007' }, + }); + }); + + act(() => { + wrapper + .find('select[data-test-subj="prioritySelect"]') + .first() + .simulate('change', { + target: { value: '2' }, + }); + }); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + await waitFor(() => - expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy() + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'jira-1', + name: 'Jira', + type: '.jira', + fields: { issueType: '10007', parent: null, priority: '2' }, + }, + }) ); }); - it('Tag options render with new tags added', async () => { + + it(`it should submit a resilient connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -172,63 +341,109 @@ describe('Create case', () => { </Router> </TestProviders> ); - await waitFor(() => + + await fillForm(wrapper); + await waitFor(() => { expect( - wrapper - .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) - .first() - .prop('options') - ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]) + wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists() + ).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect( + wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists() + ).toBeTruthy(); + }); + + act(() => { + ((wrapper.find(EuiComboBox).at(1).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + act(() => { + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + }); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'resilient-2', + name: 'My Connector 2', + type: '.resilient', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }, + }) ); }); - }); - // FAILED ES PROMOTION: https://github.com/elastic/kibana/issues/84145 - describe.skip('Step 2 - Connector Fields', () => { - const connectorTypes = [ - { - label: 'Jira', - testId: 'jira-1', - dataTestSubj: 'connector-settings-jira', - }, - { - label: 'Resilient', - testId: 'resilient-2', - dataTestSubj: 'connector-settings-resilient', - }, - { - label: 'ServiceNow', - testId: 'servicenow-1', - dataTestSubj: 'connector-settings-sn', - }, - ]; - connectorTypes.forEach(({ label, testId, dataTestSubj }) => { - it(`should change from none to ${label} connector fields`, async () => { - useConnectorsMock.mockReturnValue({ - ...sampleConnectorData, - connectors: connectorsMock, - }); + it(`it should submit a servicenow connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <Create /> - </Router> - </TestProviders> - ); - - await waitFor(() => { - expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-${testId}"]`).simulate('click'); - wrapper.update(); - }); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + + await fillForm(wrapper); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); + }); - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeTruthy(); + ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { + act(() => { + wrapper + .find(`select[data-test-subj="${subj}"]`) + .first() + .simulate('change', { + target: { value: '2' }, + }); }); }); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'servicenow-1', + name: 'My Connector', + type: '.servicenow', + fields: { impact: '2', severity: '2', urgency: '2' }, + }, + }) + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 42633c5d2ccf8..5c50c37723083 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -3,319 +3,81 @@ * 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, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPanel, - EuiSteps, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; + +import React, { useCallback } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; -import { isEqual } from 'lodash/fp'; -import { - Field, - Form, - FormDataProvider, - getUseField, - UseField, - useForm, - useFormData, -} from '../../../shared_imports'; -import { usePostCase } from '../../containers/use_post_case'; -import { schema, FormProps } from './schema'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; -import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; -import { useGetTags } from '../../containers/use_get_tags'; +import { Field, getUseField, useFormContext } from '../../../shared_imports'; import { getCaseDetailsUrl } from '../../../common/components/link_to'; -import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click'; -import { SettingFieldsForm } from '../settings/fields_form'; -import { useConnectors } from '../../containers/configure/use_connectors'; -import { ConnectorSelector } from '../connector_selector/form'; -import { useCaseConfigure } from '../../containers/configure/use_configure'; -import { - normalizeCaseConnector, - getConnectorById, - getNoneConnector, - normalizeActionConnector, -} from '../configure_cases/utils'; -import { ActionConnector } from '../../containers/types'; -import { ConnectorFields } from '../../../../../case/common/api/connectors'; import * as i18n from './translations'; +import { CreateCaseForm } from './form'; +import { FormContext } from './form_context'; +import { useInsertTimeline } from '../use_insert_timeline'; +import { fieldName as descriptionFieldName } from './description'; +import { SubmitCaseButton } from './submit_button'; export const CommonUseField = getUseField({ component: Field }); -interface ContainerProps { - big?: boolean; -} - -const Container = styled.div.attrs((props) => props)<ContainerProps>` - ${({ big, theme }) => css` - margin-top: ${big ? theme.eui.euiSizeXL : theme.eui.euiSize}; +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; `} `; -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; - z-index: 99; -`; - -const initialCaseValue: FormProps = { - description: '', - tags: [], - title: '', - connectorId: 'none', +const InsertTimeline = () => { + const { setFieldValue, getFormData } = useFormContext(); + const formData = getFormData(); + const onTimelineAttached = useCallback( + (newValue: string) => setFieldValue(descriptionFieldName, newValue), + [setFieldValue] + ); + useInsertTimeline(formData[descriptionFieldName] ?? '', onTimelineAttached); + return null; }; export const Create = React.memo(() => { const history = useHistory(); - const { caseData, isLoading, postCase } = usePostCase(); - const { loading: isLoadingConnectors, connectors } = useConnectors(); - const { connector: configureConnector, loading: isLoadingCaseConfigure } = useCaseConfigure(); - const { tags: tagOptions } = useGetTags(); - - const [connector, setConnector] = useState<ActionConnector | null>(null); - const [options, setOptions] = useState( - tagOptions.map((label) => ({ - label, - })) - ); - - // This values uses useEffect to update, not useMemo, - // because we need to setState on it from the jsx - useEffect( - () => - setOptions( - tagOptions.map((label) => ({ - label, - })) - ), - [tagOptions] - ); - - const [fields, setFields] = useState<ConnectorFields>(null); - - const { form } = useForm<FormProps>({ - defaultValue: initialCaseValue, - options: { stripEmptyFields: false }, - schema, - }); - const currentConnectorId = useMemo( - () => - !isLoadingCaseConfigure - ? normalizeCaseConnector(connectors, configureConnector)?.id ?? 'none' - : null, - [configureConnector, connectors, isLoadingCaseConfigure] - ); - const { submit, setFieldValue } = form; - const [{ description }] = useFormData<{ - description: string; - }>({ - form, - watch: ['description'], - }); - const onChangeConnector = useCallback( - (newConnectorId) => { - if (connector == null || connector.id !== newConnectorId) { - setConnector(getConnectorById(newConnectorId, connectors) ?? null); - // Reset setting fields when changing connector - setFields(null); - } + const onSuccess = useCallback( + ({ id }) => { + history.push(getCaseDetailsUrl({ id })); }, - [connector, connectors] + [history] ); - const onDescriptionChange = useCallback((newValue) => setFieldValue('description', newValue), [ - setFieldValue, - ]); - - const { handleCursorChange } = useInsertTimeline(description, onDescriptionChange); - - const handleTimelineClick = useTimelineClick(); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - const { connectorId: dataConnectorId, ...dataWithoutConnectorId } = data; - const caseConnector = getConnectorById(dataConnectorId, connectors); - const connectorToUpdate = caseConnector - ? normalizeActionConnector(caseConnector, fields) - : getNoneConnector(); - - await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate }); - } - }, [submit, postCase, fields, connectors]); - const handleSetIsCancel = useCallback(() => { history.push('/'); }, [history]); - const firstStep = useMemo( - () => ({ - title: i18n.STEP_ONE_TITLE, - children: ( - <> - <CommonUseField - path="title" - componentProps={{ - idAria: 'caseTitle', - 'data-test-subj': 'caseTitle', - euiFieldProps: { - fullWidth: false, - disabled: isLoading, - }, - }} - /> - <Container> - <CommonUseField - path="tags" - componentProps={{ - idAria: 'caseTags', - 'data-test-subj': 'caseTags', - euiFieldProps: { - fullWidth: true, - placeholder: '', - disabled: isLoading, - options, - noSuggestions: false, - }, - }} - /> - <FormDataProvider pathsToWatch="tags"> - {({ tags: anotherTags }) => { - const current: string[] = options.map((opt) => opt.label); - const newOptions = anotherTags.reduce((acc: string[], item: string) => { - if (!acc.includes(item)) { - return [...acc, item]; - } - return acc; - }, current); - if (!isEqual(current, newOptions)) { - setOptions( - newOptions.map((label: string) => ({ - label, - })) - ); - } - return null; - }} - </FormDataProvider> - </Container> - <Container big> - <UseField - path={'description'} - component={MarkdownEditorForm} - componentProps={{ - dataTestSubj: 'caseDescription', - idAria: 'caseDescription', - isDisabled: isLoading, - onClickTimeline: handleTimelineClick, - onCursorPositionUpdate: handleCursorChange, - }} - /> - </Container> - </> - ), - }), - [isLoading, options, handleCursorChange, handleTimelineClick] - ); - - const secondStep = useMemo( - () => ({ - title: i18n.STEP_TWO_TITLE, - children: ( - <EuiFlexGroup> - <EuiFlexItem> - <Container> - <UseField - path="connectorId" - component={ConnectorSelector} - componentProps={{ - connectors, - dataTestSubj: 'caseConnectors', - defaultValue: currentConnectorId, - disabled: isLoadingConnectors, - idAria: 'caseConnectors', - isLoading, - }} - onChange={onChangeConnector} - /> - </Container> - </EuiFlexItem> - <EuiFlexItem> - <Container> - <SettingFieldsForm - connector={connector} - fields={fields} - isEdit={true} - onChange={setFields} - /> - </Container> - </EuiFlexItem> - </EuiFlexGroup> - ), - }), - [ - connector, - connectors, - currentConnectorId, - fields, - isLoading, - isLoadingConnectors, - onChangeConnector, - ] - ); - - const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]); - - if (caseData != null && caseData.id) { - history.push(getCaseDetailsUrl({ id: caseData.id })); - return null; - } - return ( <EuiPanel> - {isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} - <Form form={form}> - <EuiSteps headingElement="h2" steps={allSteps} /> - </Form> - <Container> - <EuiFlexGroup - alignItems="center" - justifyContent="flexEnd" - gutterSize="xs" - responsive={false} - > - <EuiFlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="create-case-cancel" - size="s" - onClick={handleSetIsCancel} - iconType="cross" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - data-test-subj="create-case-submit" - fill - iconType="plusInCircle" - isDisabled={isLoading} - isLoading={isLoading} - onClick={onSubmit} - > - {i18n.CREATE_CASE} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </Container> + <FormContext onSuccess={onSuccess}> + <CreateCaseForm /> + <Container> + <EuiFlexGroup + alignItems="center" + justifyContent="flexEnd" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="create-case-cancel" + size="s" + onClick={handleSetIsCancel} + iconType="cross" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <SubmitCaseButton /> + </EuiFlexItem> + </EuiFlexGroup> + </Container> + <InsertTimeline /> + </FormContext> </EuiPanel> ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx new file mode 100644 index 0000000000000..3bbdb1eafd47c --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; + +import { OptionalFieldLabel } from '.'; + +describe('OptionalFieldLabel', () => { + it('it renders correctly', async () => { + const wrapper = mount(OptionalFieldLabel); + expect(wrapper.find('[data-test-subj="form-optional-field-label"]').first().text()).toBe( + 'Optional' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx index b86198e09ceac..4a491eac35d90 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import * as i18n from '../../../translations'; export const OptionalFieldLabel = ( - <EuiText color="subdued" size="xs"> + <EuiText color="subdued" size="xs" data-test-subj="form-optional-field-label"> {i18n.OPTIONAL} </EuiText> ); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index 5abac04d6ef37..a336860121c94 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasePostRequest } from '../../../../../case/common/api'; +import { CasePostRequest, ConnectorTypeFields } from '../../../../../case/common/api'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; import * as i18n from '../../translations'; @@ -18,7 +18,10 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -export type FormProps = Omit<CasePostRequest, 'connector'> & { connectorId: string }; +export type FormProps = Omit<CasePostRequest, 'connector'> & { + connectorId: string; + fields: ConnectorTypeFields['fields']; +}; export const schema: FormSchema<FormProps> = { title: { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx new file mode 100644 index 0000000000000..c8f6ebc05582f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; + +import { useForm, Form } from '../../../shared_imports'; +import { SubmitCaseButton } from './submit_button'; + +describe('SubmitCaseButton', () => { + const onSubmit = jest.fn(); + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ title: string }>({ + defaultValue: { title: 'My title' }, + onSubmit, + }); + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy(); + }); + + it('it submits', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => expect(onSubmit).toBeCalled()); + }); + + it('it disables when submitting', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isDisabled') + ).toBeTruthy(); + }); + }); + + it('it is loading when submitting', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isLoading') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx new file mode 100644 index 0000000000000..8cffce290ff11 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { EuiButton } from '@elastic/eui'; + +import { useFormContext } from '../../../shared_imports'; +import * as i18n from './translations'; + +const SubmitCaseButtonComponent: React.FC = () => { + const { submit, isSubmitting } = useFormContext(); + + return ( + <EuiButton + data-test-subj="create-case-submit" + fill + iconType="plusInCircle" + isDisabled={isSubmitting} + isLoading={isSubmitting} + onClick={submit} + > + {i18n.CREATE_CASE} + </EuiButton> + ); +}; + +export const SubmitCaseButton = memo(SubmitCaseButtonComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx new file mode 100644 index 0000000000000..c06ac011a035b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { waitFor } from '@testing-library/react'; + +import { useForm, Form, FormHook, FIELD_TYPES } from '../../../shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; +import { Tags } from './tags'; + +jest.mock('../../containers/use_get_tags'); +const useGetTagsMock = useGetTags as jest.Mock; + +describe('Tags', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ tags: string[] }>({ + defaultValue: { tags: [] }, + schema: { + tags: { type: FIELD_TYPES.COMBO_BOX }, + }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useGetTagsMock.mockReturnValue({ tags: ['test'] }); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={false} /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy(); + }); + }); + + it('it disables the input when loading', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={true} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(EuiComboBox).prop('disabled')).toBeTruthy(); + }); + + it('it changes the tags', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={false} /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(['test', 'case'].map((tag) => ({ label: tag }))); + }); + + expect(globalForm.getFormData()).toEqual({ tags: ['test', 'case'] }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/tags.tsx b/x-pack/plugins/security_solution/public/cases/components/create/tags.tsx new file mode 100644 index 0000000000000..8a7b4a6e5f760 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/tags.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo } from 'react'; + +import { Field, getUseField } from '../../../shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const TagsComponent: React.FC<Props> = ({ isLoading }) => { + const { tags: tagOptions, isLoading: isLoadingTags } = useGetTags(); + const options = useMemo( + () => + tagOptions.map((label) => ({ + label, + })), + [tagOptions] + ); + + return ( + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + disabled: isLoading || isLoadingTags, + options, + noSuggestions: false, + }, + }} + /> + ); +}; + +TagsComponent.displayName = 'TagsComponent'; + +export const Tags = memo(TagsComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/title.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/title.test.tsx new file mode 100644 index 0000000000000..54a4e665a56e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/title.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { act } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../../shared_imports'; +import { Title } from './title'; + +describe('Title', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ title: string }>({ + defaultValue: { title: 'My title' }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={false} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy(); + }); + + it('it disables the input when loading', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={true} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"] input`).prop('disabled')).toBeTruthy(); + }); + + it('it changes the title', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={false} /> + </MockHookWrapperComponent> + ); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: 'My new title' } }); + }); + + expect(globalForm.getFormData()).toEqual({ title: 'My new title' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/title.tsx b/x-pack/plugins/security_solution/public/cases/components/create/title.tsx new file mode 100644 index 0000000000000..2daeb9b738e23 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/title.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Field, getUseField } from '../../../shared_imports'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const TitleComponent: React.FC<Props> = ({ isLoading }) => ( + <CommonUseField + path="title" + componentProps={{ + idAria: 'caseTitle', + 'data-test-subj': 'caseTitle', + euiFieldProps: { + fullWidth: true, + disabled: isLoading, + }, + }} + /> +); + +TitleComponent.displayName = 'TitleComponent'; + +export const Title = memo(TitleComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx index 344ca88f5ab37..f5be9740bc4f1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx @@ -8,7 +8,7 @@ import React, { memo, useMemo } from 'react'; import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; -import { connectorsConfiguration } from '../../../common/lib/connectors/config'; +import { connectorsConfiguration } from '../connectors'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; interface ConnectorCardProps { diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx index 87536b62747e8..79ae87355b5fb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, Suspense, useCallback } from 'react'; +import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { CaseSettingsConnector, SettingFieldsProps } from './types'; @@ -18,13 +18,6 @@ interface Props extends Omit<SettingFieldsProps<ConnectorTypeFields['fields']>, const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => { const { caseSettingsRegistry } = getCaseSettings(); - const onFieldsChange = useCallback( - (newFields) => { - onChange(newFields); - }, - [onChange] - ); - if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') { return null; } @@ -45,12 +38,14 @@ const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChan </EuiFlexGroup> } > - <FieldsComponent - isEdit={isEdit} - fields={fields} - connector={connector} - onChange={onFieldsChange} - /> + <div data-test-subj={'connector-settings'}> + <FieldsComponent + isEdit={isEdit} + fields={fields} + connector={connector} + onChange={onChange} + /> + </div> </Suspense> ) : null} </> diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx index a04450b3c4198..83afa4c4f5ed3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx @@ -18,13 +18,14 @@ import { import styled, { css } from 'styled-components'; import { isEqual } from 'lodash/fp'; import * as i18n from './translations'; -import { Form, FormDataProvider, useForm } from '../../../shared_imports'; +import { Form, FormDataProvider, useForm, getUseField, Field } from '../../../shared_imports'; import { schema } from './schema'; -import { CommonUseField } from '../create'; import { useGetTags } from '../../containers/use_get_tags'; import { Tags } from './tags'; +const CommonUseField = getUseField({ component: Field }); + interface TagListProps { disabled?: boolean; isLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index 7a12f9211e969..b5885b330a822 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -22,10 +22,7 @@ export interface AllCasesModalProps { onRowClick: (id?: string) => void; } -const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ - onCloseCaseModal, - onRowClick, -}: AllCasesModalProps) => { +const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ onCloseCaseModal, onRowClick }) => { const userPermissions = useGetUserSavedObjectPermissions(); const userCanCrud = userPermissions?.crud ?? false; return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx new file mode 100644 index 0000000000000..68446fc5b3171 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback } from 'react'; +import styled from 'styled-components'; +import { + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, +} from '@elastic/eui'; + +import { FormContext } from '../create/form_context'; +import { CreateCaseForm } from '../create/form'; +import { SubmitCaseButton } from '../create/submit_button'; +import { Case } from '../../containers/types'; +import * as i18n from '../../translations'; + +export interface CreateCaseModalProps { + onCloseCaseModal: () => void; + onCaseCreated: (theCase: Case) => void; +} + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; + text-align: right; + `} +`; + +const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ + onCloseCaseModal, + onCaseCreated, +}) => { + const onSuccess = useCallback( + (theCase) => { + onCaseCreated(theCase); + onCloseCaseModal(); + }, + [onCaseCreated, onCloseCaseModal] + ); + + return ( + <EuiOverlayMask data-test-subj="all-cases-modal"> + <EuiModal onClose={onCloseCaseModal}> + <EuiModalHeader> + <EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle> + </EuiModalHeader> + <EuiModalBody> + <FormContext onSuccess={onSuccess}> + <CreateCaseForm withSteps={false} /> + <Container> + <SubmitCaseButton /> + </Container> + </FormContext> + </EuiModalBody> + </EuiModal> + </EuiOverlayMask> + ); +}; + +export const CreateCaseModal = memo(CreateModalComponent); + +CreateCaseModal.displayName = 'CreateCaseModal'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx new file mode 100644 index 0000000000000..f07be3cc60821 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { Case } from '../../containers/types'; +import { CreateCaseModal } from './create_case_modal'; + +interface Props { + onCaseCreated: (theCase: Case) => void; +} +export interface UseAllCasesModalReturnedValues { + Modal: React.FC; + isModalOpen: boolean; + closeModal: () => void; + openModal: () => void; +} + +export const useCreateCaseModal = ({ onCaseCreated }: Props) => { + const [isModalOpen, setIsModalOpen] = useState<boolean>(false); + const closeModal = useCallback(() => setIsModalOpen(false), []); + const openModal = useCallback(() => setIsModalOpen(true), []); + + const Modal: React.FC = useCallback( + () => + isModalOpen ? ( + <CreateCaseModal onCloseCaseModal={closeModal} onCaseCreated={onCaseCreated} /> + ) : null, + [closeModal, isModalOpen, onCaseCreated] + ); + + const state = useMemo( + () => ({ + Modal, + isModalOpen, + closeModal, + openModal, + }), + [isModalOpen, closeModal, openModal, Modal] + ); + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx new file mode 100644 index 0000000000000..c44193dc363a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { isEmpty } from 'lodash/fp'; + +import { getTimelineUrl, useFormatUrl } from '../../../common/components/link_to'; +import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; +import { SecurityPageName } from '../../../app/types'; +import { setInsertTimeline } from '../../../timelines/store/timeline/actions'; + +interface UseInsertTimelineReturn { + handleOnTimelineChange: (title: string, id: string | null, graphEventId?: string) => void; +} + +export const useInsertTimeline = ( + value: string, + onChange: (newValue: string) => void +): UseInsertTimelineReturn => { + const dispatch = useDispatch(); + const { formatUrl } = useFormatUrl(SecurityPageName.timelines); + + const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline); + + const handleOnTimelineChange = useCallback( + (title: string, id: string | null, graphEventId?: string) => { + const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), { + absolute: true, + skipSearch: true, + }); + + let newValue = `[${title}](${url})`; + // Leave a space between the previous value and the timeline url if the value is not empty. + if (!isEmpty(value)) { + newValue = `${value} ${newValue}`; + } + + onChange(newValue); + }, + [value, onChange, formatUrl] + ); + + useEffect(() => { + if (insertTimeline != null && value != null) { + dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); + handleOnTimelineChange( + insertTimeline.timelineTitle, + insertTimeline.timelineSavedObjectId, + insertTimeline.graphEventId + ); + dispatch(setInsertTimeline(null)); + } + }, [insertTimeline, dispatch, handleOnTimelineChange, value]); + + return { + handleOnTimelineChange, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts deleted file mode 100644 index a39e04acc1bf3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const CASE_CONNECTOR_DESC = i18n.translate( - 'xpack.securitySolution.case.components.case.selectMessageText', - { - defaultMessage: 'Create or update a case.', - } -); - -export const CASE_CONNECTOR_TITLE = i18n.translate( - 'xpack.securitySolution.case.components.case.actionTypeTitle', - { - defaultMessage: 'Cases', - } -); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f97bec65d269a..b81be3249953e 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -59,7 +59,7 @@ import { IndexFieldsStrategyResponse, } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; -import { getCaseConnectorUI } from './common/lib/connectors'; +import { getCaseConnectorUI } from './cases/components/connectors'; import { licenseService } from './common/hooks/use_license'; import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx deleted file mode 100644 index 6a65819a764eb..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useCallback, useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; - -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; -import { SecurityPageName } from '../../../../../common/constants'; -import { getTimelineUrl, useFormatUrl } from '../../../../common/components/link_to'; -import { CursorPosition } from '../../../../common/components/markdown_editor'; -import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; -import { setInsertTimeline } from '../../../store/timeline/actions'; - -export const useInsertTimeline = (value: string, onChange: (newValue: string) => void) => { - const dispatch = useDispatch(); - const { formatUrl } = useFormatUrl(SecurityPageName.timelines); - const [cursorPosition, setCursorPosition] = useState<CursorPosition>({ - start: 0, - end: 0, - }); - - const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline); - - const handleOnTimelineChange = useCallback( - (title: string, id: string | null, graphEventId?: string) => { - const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), { - absolute: true, - skipSearch: true, - }); - - const newValue: string = [ - value.slice(0, cursorPosition.start), - cursorPosition.start === cursorPosition.end - ? `[${title}](${url})` - : `[${value.slice(cursorPosition.start, cursorPosition.end)}](${url})`, - value.slice(cursorPosition.end), - ].join(''); - - onChange(newValue); - }, - [value, onChange, cursorPosition, formatUrl] - ); - - const handleCursorChange = useCallback((cp: CursorPosition) => { - setCursorPosition(cp); - }, []); - - // insertTimeline selector is defined to attached a timeline to a case outside of the case page. - // FYI, if you are in the case page we only use handleOnTimelineChange to attach a timeline to a case. - useEffect(() => { - if (insertTimeline != null && value != null) { - dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); - handleOnTimelineChange( - insertTimeline.timelineTitle, - insertTimeline.timelineSavedObjectId, - insertTimeline.graphEventId - ); - dispatch(setInsertTimeline(null)); - } - }, [insertTimeline, dispatch, handleOnTimelineChange, value]); - - return { - cursorPosition, - handleCursorChange, - handleOnTimelineChange, - }; -}; From 00a5b60779844880762af433f2ec83976ee572b3 Mon Sep 17 00:00:00 2001 From: Constance <constancecchen@users.noreply.github.com> Date: Tue, 1 Dec 2020 14:59:27 -0800 Subject: [PATCH 041/107] Attempt to more granularly separate App Search vs Workplace Search vs shared GitHub notifications (#84713) --- .github/CODEOWNERS | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 516de74378cbc..0993876f98a6a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -262,8 +262,31 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Enterprise Search # Shared -/x-pack/plugins/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/common/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/shared/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/__mocks__/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/lib/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/__mocks__/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/lib/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/routes/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/ @elastic/enterprise-search-frontend /x-pack/test/functional_enterprise_search/ @elastic/enterprise-search-frontend +# App Search +/x-pack/plugins/enterprise_search/public/applications/app_search/ @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/routes/app_search/ @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/app_search/ @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/app_search/ @elastic/app-search-frontend +# Workplace Search +/x-pack/plugins/enterprise_search/public/applications/workplace_search/ @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/routes/workplace_search/ @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/workplace_search/ @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/ @elastic/workplace-search-frontend # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui From 2c084b796facc19818eeb38301f36f79e506d299 Mon Sep 17 00:00:00 2001 From: Daniil <daniil_suleiman@epam.com> Date: Wed, 2 Dec 2020 11:02:31 +0300 Subject: [PATCH 042/107] [Input Control] Custom renderer (#84423) * Create custom renderer * Reduce initial bundle size * Fix tests * Add unit test * Remove injectI18n usage Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../input_control_fn.test.ts.snap | 2 +- .../public/__snapshots__/to_ast.test.ts.snap | 18 + .../public/components/editor/_index.scss | 1 - ...ontrol_editor.scss => control_editor.scss} | 0 .../components/editor/control_editor.tsx | 2 + .../components/editor/controls_tab.test.tsx | 18 +- .../public/components/editor/controls_tab.tsx | 68 +- .../public/components/editor/index.tsx | 34 + .../components/editor/options_tab.test.tsx | 6 +- .../public/components/editor/options_tab.tsx | 18 +- .../input_control_vis.test.tsx.snap | 610 +++++++++--------- .../public/components/vis/_index.scss | 1 - .../public/components/vis/_vis.scss | 5 - .../components/vis/input_control_vis.scss | 13 + .../components/vis/input_control_vis.tsx | 10 +- .../input_control_vis/public/index.scss | 9 - src/plugins/input_control_vis/public/index.ts | 2 - .../public/input_control_fn.ts | 19 +- .../public/input_control_vis_renderer.tsx | 51 ++ .../public/input_control_vis_type.ts | 14 +- .../input_control_vis/public/plugin.ts | 2 + .../input_control_vis/public/to_ast.test.ts | 54 ++ .../input_control_vis/public/to_ast.ts | 36 ++ src/plugins/input_control_vis/public/types.ts | 27 + .../public/vis_controller.tsx | 65 +- .../__snapshots__/build_pipeline.test.ts.snap | 2 - .../public/legacy/build_pipeline.test.ts | 9 - .../public/legacy/build_pipeline.ts | 3 - 28 files changed, 663 insertions(+), 436 deletions(-) create mode 100644 src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap delete mode 100644 src/plugins/input_control_vis/public/components/editor/_index.scss rename src/plugins/input_control_vis/public/components/editor/{_control_editor.scss => control_editor.scss} (100%) create mode 100644 src/plugins/input_control_vis/public/components/editor/index.tsx delete mode 100644 src/plugins/input_control_vis/public/components/vis/_index.scss delete mode 100644 src/plugins/input_control_vis/public/components/vis/_vis.scss create mode 100644 src/plugins/input_control_vis/public/components/vis/input_control_vis.scss delete mode 100644 src/plugins/input_control_vis/public/index.scss create mode 100644 src/plugins/input_control_vis/public/input_control_vis_renderer.tsx create mode 100644 src/plugins/input_control_vis/public/to_ast.test.ts create mode 100644 src/plugins/input_control_vis/public/to_ast.ts create mode 100644 src/plugins/input_control_vis/public/types.ts diff --git a/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap b/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap index 35349b4719676..696b74d040e0c 100644 --- a/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap +++ b/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap @@ -2,7 +2,7 @@ exports[`interpreter/functions#input_control_vis returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "input_control_vis", "type": "render", "value": Object { "visConfig": Object { diff --git a/src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..edd44d8dd0337 --- /dev/null +++ b/src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`input_control_vis toExpressionAst should build an expression based on vis.params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"controls\\":[{\\"id\\":\\"1536977437774\\",\\"fieldName\\":\\"manufacturer.keyword\\",\\"parent\\":\\"\\",\\"label\\":\\"Manufacturer\\",\\"type\\":\\"list\\",\\"options\\":{\\"type\\":\\"terms\\",\\"multiselect\\":true,\\"dynamicOptions\\":true,\\"size\\":5,\\"order\\":\\"desc\\"},\\"indexPattern\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\"}],\\"updateFiltersOnChange\\":false,\\"useTimeFilter\\":true,\\"pinFilters\\":false}", + ], + }, + "function": "input_control_vis", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/input_control_vis/public/components/editor/_index.scss b/src/plugins/input_control_vis/public/components/editor/_index.scss deleted file mode 100644 index 9af8f8d6e8222..0000000000000 --- a/src/plugins/input_control_vis/public/components/editor/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './control_editor'; diff --git a/src/plugins/input_control_vis/public/components/editor/_control_editor.scss b/src/plugins/input_control_vis/public/components/editor/control_editor.scss similarity index 100% rename from src/plugins/input_control_vis/public/components/editor/_control_editor.scss rename to src/plugins/input_control_vis/public/components/editor/control_editor.scss diff --git a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx index aa473095aaf3f..109237f8db4ec 100644 --- a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -36,6 +36,8 @@ import { getTitle, ControlParams, CONTROL_TYPES, ControlParamsOptions } from '.. import { IIndexPattern } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; +import './control_editor.scss'; + interface ControlEditorUiProps { controlIndex: number; controlParams: ControlParams; diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index a85f98c7b89ba..c05dec8fccbe1 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -21,16 +21,16 @@ import React from 'react'; import { shallowWithIntl, mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; -import { ControlsTab, ControlsTabUiProps } from './controls_tab'; +import ControlsTab, { ControlsTabProps } from './controls_tab'; import { Vis } from '../../../../visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, }; -let props: ControlsTabUiProps; +let props: ControlsTabProps; beforeEach(() => { - props = { + props = ({ deps: getDepsMock(), vis: ({ API: { @@ -78,18 +78,18 @@ beforeEach(() => { }, setValue: jest.fn(), intl: null as any, - }; + } as unknown) as ControlsTabProps; }); test('renders ControlsTab', () => { - const component = shallowWithIntl(<ControlsTab.WrappedComponent {...props} />); + const component = shallowWithIntl(<ControlsTab {...props} />); expect(component).toMatchSnapshot(); }); describe('behavior', () => { test('add control button', () => { - const component = mountWithIntl(<ControlsTab.WrappedComponent {...props} />); + const component = mountWithIntl(<ControlsTab {...props} />); findTestSubject(component, 'inputControlEditorAddBtn').simulate('click'); @@ -102,7 +102,7 @@ describe('behavior', () => { }); test('remove control button', () => { - const component = mountWithIntl(<ControlsTab.WrappedComponent {...props} />); + const component = mountWithIntl(<ControlsTab {...props} />); findTestSubject(component, 'inputControlEditorRemoveControl0').simulate('click'); const expectedParams = [ 'controls', @@ -125,7 +125,7 @@ describe('behavior', () => { }); test('move down control button', () => { - const component = mountWithIntl(<ControlsTab.WrappedComponent {...props} />); + const component = mountWithIntl(<ControlsTab {...props} />); findTestSubject(component, 'inputControlEditorMoveDownControl0').simulate('click'); const expectedParams = [ 'controls', @@ -162,7 +162,7 @@ describe('behavior', () => { }); test('move up control button', () => { - const component = mountWithIntl(<ControlsTab.WrappedComponent {...props} />); + const component = mountWithIntl(<ControlsTab {...props} />); findTestSubject(component, 'inputControlEditorMoveUpControl1').simulate('click'); const expectedParams = [ 'controls', diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx index a9f04a86f8d03..0e622e08c529f 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -18,7 +18,8 @@ */ import React, { PureComponent } from 'react'; -import { injectI18n, FormattedMessage, InjectedIntlProps } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -44,22 +45,17 @@ import { } from '../../editor_utils'; import { getLineageMap, getParentCandidates } from '../../lineage'; import { InputControlVisDependencies } from '../../plugin'; +import { InputControlVisParams } from '../../types'; interface ControlsTabUiState { type: CONTROL_TYPES; } -interface ControlsTabUiParams { - controls: ControlParams[]; -} -type ControlsTabUiInjectedProps = InjectedIntlProps & - Pick<VisOptionsProps<ControlsTabUiParams>, 'vis' | 'stateParams' | 'setValue'> & { - deps: InputControlVisDependencies; - }; +export type ControlsTabProps = VisOptionsProps<InputControlVisParams> & { + deps: InputControlVisDependencies; +}; -export type ControlsTabUiProps = ControlsTabUiInjectedProps; - -class ControlsTabUi extends PureComponent<ControlsTabUiProps, ControlsTabUiState> { +class ControlsTab extends PureComponent<ControlsTabProps, ControlsTabUiState> { state = { type: CONTROL_TYPES.LIST, }; @@ -161,8 +157,6 @@ class ControlsTabUi extends PureComponent<ControlsTabUiProps, ControlsTabUiState } render() { - const { intl } = this.props; - return ( <div> {this.renderControls()} @@ -176,25 +170,31 @@ class ControlsTabUi extends PureComponent<ControlsTabUiProps, ControlsTabUiState options={[ { value: CONTROL_TYPES.RANGE, - text: intl.formatMessage({ - id: 'inputControl.editor.controlsTab.select.rangeDropDownOptionLabel', - defaultMessage: 'Range slider', - }), + text: i18n.translate( + 'inputControl.editor.controlsTab.select.rangeDropDownOptionLabel', + { + defaultMessage: 'Range slider', + } + ), }, { value: CONTROL_TYPES.LIST, - text: intl.formatMessage({ - id: 'inputControl.editor.controlsTab.select.listDropDownOptionLabel', - defaultMessage: 'Options list', - }), + text: i18n.translate( + 'inputControl.editor.controlsTab.select.listDropDownOptionLabel', + { + defaultMessage: 'Options list', + } + ), }, ]} value={this.state.type} onChange={(event) => this.setState({ type: event.target.value as CONTROL_TYPES })} - aria-label={intl.formatMessage({ - id: 'inputControl.editor.controlsTab.select.controlTypeAriaLabel', - defaultMessage: 'Select control type', - })} + aria-label={i18n.translate( + 'inputControl.editor.controlsTab.select.controlTypeAriaLabel', + { + defaultMessage: 'Select control type', + } + )} /> </EuiFormRow> </EuiFlexItem> @@ -205,10 +205,12 @@ class ControlsTabUi extends PureComponent<ControlsTabUiProps, ControlsTabUiState onClick={this.handleAddControl} iconType="plusInCircle" data-test-subj="inputControlEditorAddBtn" - aria-label={intl.formatMessage({ - id: 'inputControl.editor.controlsTab.select.addControlAriaLabel', - defaultMessage: 'Add control', - })} + aria-label={i18n.translate( + 'inputControl.editor.controlsTab.select.addControlAriaLabel', + { + defaultMessage: 'Add control', + } + )} > <FormattedMessage id="inputControl.editor.controlsTab.addButtonLabel" @@ -224,8 +226,6 @@ class ControlsTabUi extends PureComponent<ControlsTabUiProps, ControlsTabUiState } } -export const ControlsTab = injectI18n(ControlsTabUi); - -export const getControlsTab = (deps: InputControlVisDependencies) => ( - props: Omit<ControlsTabUiProps, 'core'> -) => <ControlsTab {...props} deps={deps} />; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { ControlsTab as default }; diff --git a/src/plugins/input_control_vis/public/components/editor/index.tsx b/src/plugins/input_control_vis/public/components/editor/index.tsx new file mode 100644 index 0000000000000..11b3c2ea4ee8a --- /dev/null +++ b/src/plugins/input_control_vis/public/components/editor/index.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { InputControlVisDependencies } from '../../plugin'; +import { InputControlVisParams } from '../../types'; + +const ControlsTab = lazy(() => import('./controls_tab')); +const OptionsTab = lazy(() => import('./options_tab')); + +export const getControlsTab = (deps: InputControlVisDependencies) => ( + props: VisOptionsProps<InputControlVisParams> +) => <ControlsTab {...props} deps={deps} />; + +export const OptionsTabLazy = (props: VisOptionsProps<InputControlVisParams>) => ( + <OptionsTab {...props} /> +); diff --git a/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx index 0f126e915a68c..0970d1cd3c298 100644 --- a/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx @@ -22,13 +22,13 @@ import { shallow } from 'enzyme'; import { mountWithIntl } from '@kbn/test/jest'; import { Vis } from '../../../../visualizations/public'; -import { OptionsTab, OptionsTabProps } from './options_tab'; +import OptionsTab, { OptionsTabProps } from './options_tab'; describe('OptionsTab', () => { let props: OptionsTabProps; beforeEach(() => { - props = { + props = ({ vis: {} as Vis, stateParams: { updateFiltersOnChange: false, @@ -36,7 +36,7 @@ describe('OptionsTab', () => { pinFilters: false, }, setValue: jest.fn(), - }; + } as unknown) as OptionsTabProps; }); it('should renders OptionsTab', () => { diff --git a/src/plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx index cdff6cabad8ba..306d1141e75bd 100644 --- a/src/plugins/input_control_vis/public/components/editor/options_tab.tsx +++ b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx @@ -24,20 +24,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSwitchEvent } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { InputControlVisParams } from '../../types'; -interface OptionsTabParams { - updateFiltersOnChange: boolean; - useTimeFilter: boolean; - pinFilters: boolean; -} -type OptionsTabInjectedProps = Pick< - VisOptionsProps<OptionsTabParams>, - 'vis' | 'setValue' | 'stateParams' ->; - -export type OptionsTabProps = OptionsTabInjectedProps; +export type OptionsTabProps = VisOptionsProps<InputControlVisParams>; -export class OptionsTab extends PureComponent<OptionsTabProps> { +class OptionsTab extends PureComponent<OptionsTabProps> { handleUpdateFiltersChange = (event: EuiSwitchEvent) => { this.props.setValue('updateFiltersOnChange', event.target.checked); }; @@ -98,3 +89,6 @@ export class OptionsTab extends PureComponent<OptionsTabProps> { ); } } +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { OptionsTab as default }; diff --git a/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap index 5a76967c71fbb..5e1f25993616b 100644 --- a/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap @@ -2,355 +2,371 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = ` <div - className="icvContainer" + className="icvContainer__wrapper" > - <EuiFlexGroup - wrap={true} + <div + className="icvContainer" > - <EuiFlexItem - data-test-subj="inputControlItem" - key="mock-list-control" - style={ - Object { - "minWidth": "250px", - } - } + <EuiFlexGroup + wrap={true} > - <InjectIntl(ListControlUi) - controlIndex={0} - fetchOptions={[Function]} - formatOptionLabel={[Function]} - id="mock-list-control" - label="list control" - multiselect={true} - options={ - Array [ - "choice1", - "choice2", - ] + <EuiFlexItem + data-test-subj="inputControlItem" + key="mock-list-control" + style={ + Object { + "minWidth": "250px", + } } - selectedOptions={Array []} - stageFilter={[Function]} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup - wrap={true} - > - <EuiFlexItem - grow={false} - > - <EuiButton - data-test-subj="inputControlSubmitBtn" - disabled={false} - fill={true} - onClick={[Function]} > - <FormattedMessage - defaultMessage="Apply changes" - id="inputControl.vis.inputControlVis.applyChangesButtonLabel" - values={Object {}} + <InjectIntl(ListControlUi) + controlIndex={0} + fetchOptions={[Function]} + formatOptionLabel={[Function]} + id="mock-list-control" + label="list control" + multiselect={true} + options={ + Array [ + "choice1", + "choice2", + ] + } + selectedOptions={Array []} + stageFilter={[Function]} /> - </EuiButton> - </EuiFlexItem> - <EuiFlexItem - grow={false} + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup + wrap={true} > - <EuiButtonEmpty - data-test-subj="inputControlCancelBtn" - disabled={false} - onClick={[Function]} + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Cancel changes" - id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiButtonEmpty - data-test-subj="inputControlClearBtn" - disabled={true} - onClick={[Function]} + <EuiButton + data-test-subj="inputControlSubmitBtn" + disabled={false} + fill={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Apply changes" + id="inputControl.vis.inputControlVis.applyChangesButtonLabel" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Clear form" - id="inputControl.vis.inputControlVis.clearFormButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> + <EuiButtonEmpty + data-test-subj="inputControlCancelBtn" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Cancel changes" + id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButtonEmpty + data-test-subj="inputControlClearBtn" + disabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Clear form" + id="inputControl.vis.inputControlVis.clearFormButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </div> </div> `; exports[`Clear btns enabled when there are values 1`] = ` <div - className="icvContainer" + className="icvContainer__wrapper" > - <EuiFlexGroup - wrap={true} + <div + className="icvContainer" > - <EuiFlexItem - data-test-subj="inputControlItem" - key="mock-list-control" - style={ - Object { - "minWidth": "250px", - } - } + <EuiFlexGroup + wrap={true} > - <InjectIntl(ListControlUi) - controlIndex={0} - fetchOptions={[Function]} - formatOptionLabel={[Function]} - id="mock-list-control" - label="list control" - multiselect={true} - options={ - Array [ - "choice1", - "choice2", - ] + <EuiFlexItem + data-test-subj="inputControlItem" + key="mock-list-control" + style={ + Object { + "minWidth": "250px", + } } - selectedOptions={Array []} - stageFilter={[Function]} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup - wrap={true} - > - <EuiFlexItem - grow={false} - > - <EuiButton - data-test-subj="inputControlSubmitBtn" - disabled={true} - fill={true} - onClick={[Function]} > - <FormattedMessage - defaultMessage="Apply changes" - id="inputControl.vis.inputControlVis.applyChangesButtonLabel" - values={Object {}} + <InjectIntl(ListControlUi) + controlIndex={0} + fetchOptions={[Function]} + formatOptionLabel={[Function]} + id="mock-list-control" + label="list control" + multiselect={true} + options={ + Array [ + "choice1", + "choice2", + ] + } + selectedOptions={Array []} + stageFilter={[Function]} /> - </EuiButton> - </EuiFlexItem> - <EuiFlexItem - grow={false} + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup + wrap={true} > - <EuiButtonEmpty - data-test-subj="inputControlCancelBtn" - disabled={true} - onClick={[Function]} + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Cancel changes" - id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiButtonEmpty - data-test-subj="inputControlClearBtn" - disabled={false} - onClick={[Function]} + <EuiButton + data-test-subj="inputControlSubmitBtn" + disabled={true} + fill={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Apply changes" + id="inputControl.vis.inputControlVis.applyChangesButtonLabel" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Clear form" - id="inputControl.vis.inputControlVis.clearFormButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> + <EuiButtonEmpty + data-test-subj="inputControlCancelBtn" + disabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Cancel changes" + id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButtonEmpty + data-test-subj="inputControlClearBtn" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Clear form" + id="inputControl.vis.inputControlVis.clearFormButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </div> </div> `; exports[`Renders list control 1`] = ` <div - className="icvContainer" + className="icvContainer__wrapper" > - <EuiFlexGroup - wrap={true} + <div + className="icvContainer" > - <EuiFlexItem - data-test-subj="inputControlItem" - key="mock-list-control" - style={ - Object { - "minWidth": "250px", - } - } + <EuiFlexGroup + wrap={true} > - <InjectIntl(ListControlUi) - controlIndex={0} - fetchOptions={[Function]} - formatOptionLabel={[Function]} - id="mock-list-control" - label="list control" - multiselect={true} - options={ - Array [ - "choice1", - "choice2", - ] + <EuiFlexItem + data-test-subj="inputControlItem" + key="mock-list-control" + style={ + Object { + "minWidth": "250px", + } } - selectedOptions={Array []} - stageFilter={[Function]} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup - wrap={true} - > - <EuiFlexItem - grow={false} - > - <EuiButton - data-test-subj="inputControlSubmitBtn" - disabled={true} - fill={true} - onClick={[Function]} > - <FormattedMessage - defaultMessage="Apply changes" - id="inputControl.vis.inputControlVis.applyChangesButtonLabel" - values={Object {}} + <InjectIntl(ListControlUi) + controlIndex={0} + fetchOptions={[Function]} + formatOptionLabel={[Function]} + id="mock-list-control" + label="list control" + multiselect={true} + options={ + Array [ + "choice1", + "choice2", + ] + } + selectedOptions={Array []} + stageFilter={[Function]} /> - </EuiButton> - </EuiFlexItem> - <EuiFlexItem - grow={false} + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup + wrap={true} > - <EuiButtonEmpty - data-test-subj="inputControlCancelBtn" - disabled={true} - onClick={[Function]} + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Cancel changes" - id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiButtonEmpty - data-test-subj="inputControlClearBtn" - disabled={true} - onClick={[Function]} + <EuiButton + data-test-subj="inputControlSubmitBtn" + disabled={true} + fill={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Apply changes" + id="inputControl.vis.inputControlVis.applyChangesButtonLabel" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Clear form" - id="inputControl.vis.inputControlVis.clearFormButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> + <EuiButtonEmpty + data-test-subj="inputControlCancelBtn" + disabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Cancel changes" + id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButtonEmpty + data-test-subj="inputControlClearBtn" + disabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Clear form" + id="inputControl.vis.inputControlVis.clearFormButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </div> </div> `; exports[`Renders range control 1`] = ` <div - className="icvContainer" + className="icvContainer__wrapper" > - <EuiFlexGroup - wrap={true} + <div + className="icvContainer" > - <EuiFlexItem - data-test-subj="inputControlItem" - key="mock-range-control" - style={ - Object { - "minWidth": "250px", - } - } + <EuiFlexGroup + wrap={true} > - <RangeControl - control={ + <EuiFlexItem + data-test-subj="inputControlItem" + key="mock-range-control" + style={ Object { - "format": [Function], - "id": "mock-range-control", - "isEnabled": [Function], - "label": "range control", - "max": 100, - "min": 0, - "options": Object { - "decimalPlaces": 0, - "step": 1, - }, - "type": "range", - "value": Object { - "max": 0, - "min": 0, - }, + "minWidth": "250px", } } - controlIndex={0} - stageFilter={[Function]} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup - wrap={true} - > - <EuiFlexItem - grow={false} - > - <EuiButton - data-test-subj="inputControlSubmitBtn" - disabled={true} - fill={true} - onClick={[Function]} > - <FormattedMessage - defaultMessage="Apply changes" - id="inputControl.vis.inputControlVis.applyChangesButtonLabel" - values={Object {}} + <RangeControl + control={ + Object { + "format": [Function], + "id": "mock-range-control", + "isEnabled": [Function], + "label": "range control", + "max": 100, + "min": 0, + "options": Object { + "decimalPlaces": 0, + "step": 1, + }, + "type": "range", + "value": Object { + "max": 0, + "min": 0, + }, + } + } + controlIndex={0} + stageFilter={[Function]} /> - </EuiButton> - </EuiFlexItem> - <EuiFlexItem - grow={false} + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup + wrap={true} > - <EuiButtonEmpty - data-test-subj="inputControlCancelBtn" - disabled={true} - onClick={[Function]} + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Cancel changes" - id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiButtonEmpty - data-test-subj="inputControlClearBtn" - disabled={true} - onClick={[Function]} + <EuiButton + data-test-subj="inputControlSubmitBtn" + disabled={true} + fill={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Apply changes" + id="inputControl.vis.inputControlVis.applyChangesButtonLabel" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} > - <FormattedMessage - defaultMessage="Clear form" - id="inputControl.vis.inputControlVis.clearFormButtonLabel" - values={Object {}} - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> + <EuiButtonEmpty + data-test-subj="inputControlCancelBtn" + disabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Cancel changes" + id="inputControl.vis.inputControlVis.cancelChangesButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButtonEmpty + data-test-subj="inputControlClearBtn" + disabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Clear form" + id="inputControl.vis.inputControlVis.clearFormButtonLabel" + values={Object {}} + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </div> </div> `; diff --git a/src/plugins/input_control_vis/public/components/vis/_index.scss b/src/plugins/input_control_vis/public/components/vis/_index.scss deleted file mode 100644 index a428a7c1782e3..0000000000000 --- a/src/plugins/input_control_vis/public/components/vis/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './vis'; diff --git a/src/plugins/input_control_vis/public/components/vis/_vis.scss b/src/plugins/input_control_vis/public/components/vis/_vis.scss deleted file mode 100644 index d42c2c5f263c7..0000000000000 --- a/src/plugins/input_control_vis/public/components/vis/_vis.scss +++ /dev/null @@ -1,5 +0,0 @@ -.icvContainer { - width: 100%; - margin: 0 $euiSizeXS; - padding: $euiSizeS; -} diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss b/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss new file mode 100644 index 0000000000000..322573446f762 --- /dev/null +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss @@ -0,0 +1,13 @@ +.icvContainer__wrapper { + @include euiScrollBar; + min-height: 0; + flex: 1 1 0; + display: flex; + overflow: auto; +} + +.icvContainer { + width: 100%; + margin: 0 $euiSizeXS; + padding: $euiSizeS; +} diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx index 95edb4a35bc22..058f39cb8a6d4 100644 --- a/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -26,6 +26,8 @@ import { RangeControl } from '../../control/range_control_factory'; import { ListControl as ListControlComponent } from './list_control'; import { RangeControl as RangeControlComponent } from './range_control'; +import './input_control_vis.scss'; + function isListControl(control: RangeControl | ListControl): control is ListControl { return control.type === CONTROL_TYPES.LIST; } @@ -165,9 +167,11 @@ export class InputControlVis extends Component<InputControlVisProps> { } return ( - <div className="icvContainer"> - <EuiFlexGroup wrap>{this.renderControls()}</EuiFlexGroup> - {stagingButtons} + <div className="icvContainer__wrapper"> + <div className="icvContainer"> + <EuiFlexGroup wrap>{this.renderControls()}</EuiFlexGroup> + {stagingButtons} + </div> </div> ); } diff --git a/src/plugins/input_control_vis/public/index.scss b/src/plugins/input_control_vis/public/index.scss deleted file mode 100644 index 42fded23d7761..0000000000000 --- a/src/plugins/input_control_vis/public/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Prefix all styles with "icv" to avoid conflicts. -// Examples -// icvChart -// icvChart__legend -// icvChart__legend--small -// icvChart__legend-isLoading - -@import './components/editor/index'; -@import './components/vis/index'; diff --git a/src/plugins/input_control_vis/public/index.ts b/src/plugins/input_control_vis/public/index.ts index 8edd3fd9996c3..b6fee12f6d9cb 100644 --- a/src/plugins/input_control_vis/public/index.ts +++ b/src/plugins/input_control_vis/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import './index.scss'; - import { PluginInitializerContext } from '../../../core/public'; import { InputControlVisPlugin as Plugin } from './plugin'; diff --git a/src/plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts index 1664555b916b6..46fba66264bcb 100644 --- a/src/plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -20,24 +20,25 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; +import { InputControlVisParams } from './types'; interface Arguments { visConfig: string; } -type VisParams = Required<Arguments>; - -interface RenderValue { +export interface InputControlRenderValue { visType: 'input_control_vis'; - visConfig: VisParams; + visConfig: InputControlVisParams; } -export const createInputControlVisFn = (): ExpressionFunctionDefinition< +export type InputControlExpressionFunctionDefinition = ExpressionFunctionDefinition< 'input_control_vis', Datatable, Arguments, - Render<RenderValue> -> => ({ + Render<InputControlRenderValue> +>; + +export const createInputControlVisFn = (): InputControlExpressionFunctionDefinition => ({ name: 'input_control_vis', type: 'render', inputTypes: [], @@ -52,10 +53,10 @@ export const createInputControlVisFn = (): ExpressionFunctionDefinition< }, }, fn(input, args) { - const params = JSON.parse(args.visConfig); + const params: InputControlVisParams = JSON.parse(args.visConfig); return { type: 'render', - as: 'visualization', + as: 'input_control_vis', value: { visType: 'input_control_vis', visConfig: params, diff --git a/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx new file mode 100644 index 0000000000000..6431ed6ebed1e --- /dev/null +++ b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { InputControlVisDependencies } from './plugin'; +import { InputControlRenderValue } from './input_control_fn'; +import type { InputControlVisControllerType } from './vis_controller'; + +const inputControlVisRegistry = new Map<HTMLElement, InputControlVisControllerType>(); + +export const getInputControlVisRenderer: ( + deps: InputControlVisDependencies +) => ExpressionRenderDefinition<InputControlRenderValue> = (deps) => ({ + name: 'input_control_vis', + reuseDomNode: true, + render: async (domNode, { visConfig }, handlers) => { + let registeredController = inputControlVisRegistry.get(domNode); + + if (!registeredController) { + const { createInputControlVisController } = await import('./vis_controller'); + + const Controller = createInputControlVisController(deps, handlers); + registeredController = new Controller(domNode); + inputControlVisRegistry.set(domNode, registeredController); + + handlers.onDestroy(() => { + registeredController?.destroy(); + inputControlVisRegistry.delete(domNode); + }); + } + + await registeredController.render(visConfig); + handlers.done(); + }, +}); diff --git a/src/plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts index 6e33e18c1603b..686327a1ba774 100644 --- a/src/plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -19,15 +19,14 @@ import { i18n } from '@kbn/i18n'; import { VisGroups, BaseVisTypeOptions } from '../../visualizations/public'; -import { createInputControlVisController } from './vis_controller'; -import { getControlsTab } from './components/editor/controls_tab'; -import { OptionsTab } from './components/editor/options_tab'; +import { getControlsTab, OptionsTabLazy } from './components/editor'; import { InputControlVisDependencies } from './plugin'; +import { toExpressionAst } from './to_ast'; +import { InputControlVisParams } from './types'; export function createInputControlVisTypeDefinition( deps: InputControlVisDependencies -): BaseVisTypeOptions { - const InputControlVisController = createInputControlVisController(deps); +): BaseVisTypeOptions<InputControlVisParams> { const ControlsTab = getControlsTab(deps); return { @@ -41,7 +40,6 @@ export function createInputControlVisTypeDefinition( defaultMessage: 'Add dropdown menus and range sliders to your dashboard.', }), stage: 'experimental', - visualization: InputControlVisController, visConfig: { defaults: { controls: [], @@ -64,12 +62,12 @@ export function createInputControlVisTypeDefinition( title: i18n.translate('inputControl.register.tabs.optionsTitle', { defaultMessage: 'Options', }), - editor: OptionsTab, + editor: OptionsTabLazy, }, ], }, inspectorAdapters: {}, requestHandler: 'none', - responseHandler: 'none', + toExpressionAst, }; } diff --git a/src/plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts index 2c93a529c25b1..afaaa27d74c82 100644 --- a/src/plugins/input_control_vis/public/plugin.ts +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/p import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { createInputControlVisFn } from './input_control_fn'; +import { getInputControlVisRenderer } from './input_control_vis_renderer'; import { createInputControlVisTypeDefinition } from './input_control_vis_type'; type InputControlVisCoreSetup = CoreSetup<InputControlVisPluginStartDependencies, void>; @@ -76,6 +77,7 @@ export class InputControlVisPlugin implements Plugin<void, void> { }; expressions.registerFunction(createInputControlVisFn); + expressions.registerRenderer(getInputControlVisRenderer(visualizationDependencies)); visualizations.createBaseVisualization( createInputControlVisTypeDefinition(visualizationDependencies) ); diff --git a/src/plugins/input_control_vis/public/to_ast.test.ts b/src/plugins/input_control_vis/public/to_ast.test.ts new file mode 100644 index 0000000000000..fbeb78ee93a1e --- /dev/null +++ b/src/plugins/input_control_vis/public/to_ast.test.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { InputControlVisParams } from './types'; +import { toExpressionAst } from './to_ast'; + +describe('input_control_vis toExpressionAst', () => { + const vis = { + params: { + controls: [ + { + id: '1536977437774', + fieldName: 'manufacturer.keyword', + parent: '', + label: 'Manufacturer', + type: 'list', + options: { + type: 'terms', + multiselect: true, + dynamicOptions: true, + size: 5, + order: 'desc', + }, + indexPattern: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + }, + ], + updateFiltersOnChange: false, + useTimeFilter: true, + pinFilters: false, + }, + } as Vis<InputControlVisParams>; + + it('should build an expression based on vis.params', () => { + const expression = toExpressionAst(vis); + expect(expression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/input_control_vis/public/to_ast.ts b/src/plugins/input_control_vis/public/to_ast.ts new file mode 100644 index 0000000000000..93c0b4a87cfe6 --- /dev/null +++ b/src/plugins/input_control_vis/public/to_ast.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { Vis } from '../../visualizations/public'; +import { InputControlExpressionFunctionDefinition } from './input_control_fn'; +import { InputControlVisParams } from './types'; + +export const toExpressionAst = (vis: Vis<InputControlVisParams>) => { + const inputControl = buildExpressionFunction<InputControlExpressionFunctionDefinition>( + 'input_control_vis', + { + visConfig: JSON.stringify(vis.params), + } + ); + + const ast = buildExpression([inputControl]); + + return ast.toAst(); +}; diff --git a/src/plugins/input_control_vis/public/types.ts b/src/plugins/input_control_vis/public/types.ts new file mode 100644 index 0000000000000..2898ab49590ed --- /dev/null +++ b/src/plugins/input_control_vis/public/types.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ControlParams } from './editor_utils'; + +export interface InputControlVisParams { + controls: ControlParams[]; + pinFilters: boolean; + updateFiltersOnChange: boolean; + useTimeFilter: boolean; +} diff --git a/src/plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx index 6f35e17866120..8e762a38671e9 100644 --- a/src/plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -20,20 +20,29 @@ import React from 'react'; import { isEqual } from 'lodash'; import { render, unmountComponentAtNode } from 'react-dom'; - import { Subscription } from 'rxjs'; + import { I18nStart } from 'kibana/public'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { VisualizationContainer } from '../../visualizations/public'; +import { FilterManager, Filter } from '../../data/public'; + import { InputControlVis } from './components/vis/input_control_vis'; import { getControlFactory } from './control/control_factory'; import { getLineageMap } from './lineage'; -import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../data/public'; -import { VisParams, ExprVis } from '../../visualizations/public'; +import { InputControlVisParams } from './types'; -export const createInputControlVisController = (deps: InputControlVisDependencies) => { +export type InputControlVisControllerType = InstanceType< + ReturnType<typeof createInputControlVisController> +>; + +export const createInputControlVisController = ( + deps: InputControlVisDependencies, + handlers: IInterpreterRenderHandlers +) => { return class InputControlVisController { private I18nContext?: I18nStart['Context']; private _isLoaded = false; @@ -43,9 +52,9 @@ export const createInputControlVisController = (deps: InputControlVisDependencie filterManager: FilterManager; updateSubsciption: any; timeFilterSubscription: Subscription; - visParams?: VisParams; + visParams?: InputControlVisParams; - constructor(public el: Element, public vis: ExprVis) { + constructor(public el: Element) { this.controls = []; this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); @@ -63,7 +72,7 @@ export const createInputControlVisController = (deps: InputControlVisDependencie }); } - async render(visData: any, visParams: VisParams) { + async render(visParams: InputControlVisParams) { if (!this.I18nContext) { const [{ i18n }] = await deps.core.getStartServices(); this.I18nContext = i18n.Context; @@ -71,7 +80,7 @@ export const createInputControlVisController = (deps: InputControlVisDependencie if (!this._isLoaded || !isEqual(visParams, this.visParams)) { this.visParams = visParams; this.controls = []; - this.controls = await this.initControls(); + this.controls = await this.initControls(visParams); this._isLoaded = true; } this.drawVis(); @@ -91,34 +100,34 @@ export const createInputControlVisController = (deps: InputControlVisDependencie render( <this.I18nContext> - <InputControlVis - updateFiltersOnChange={this.visParams?.updateFiltersOnChange} - controls={this.controls} - stageFilter={this.stageFilter} - submitFilters={this.submitFilters} - resetControls={this.updateControlsFromKbn} - clearControls={this.clearControls} - hasChanges={this.hasChanges} - hasValues={this.hasValues} - refreshControl={this.refreshControl} - /> + <VisualizationContainer handlers={handlers}> + <InputControlVis + updateFiltersOnChange={this.visParams?.updateFiltersOnChange} + controls={this.controls} + stageFilter={this.stageFilter} + submitFilters={this.submitFilters} + resetControls={this.updateControlsFromKbn} + clearControls={this.clearControls} + hasChanges={this.hasChanges} + hasValues={this.hasValues} + refreshControl={this.refreshControl} + /> + </VisualizationContainer> </this.I18nContext>, this.el ); }; - async initControls() { - const controlParamsList = (this.visParams?.controls as ControlParams[])?.filter( - (controlParams) => { - // ignore controls that do not have indexPattern or field - return controlParams.indexPattern && controlParams.fieldName; - } - ); + async initControls(visParams: InputControlVisParams) { + const controlParamsList = visParams.controls.filter((controlParams) => { + // ignore controls that do not have indexPattern or field + return controlParams.indexPattern && controlParams.fieldName; + }); const controlFactoryPromises = controlParamsList.map((controlParams) => { const factory = getControlFactory(controlParams); - return factory(controlParams, this.visParams?.useTimeFilter, deps); + return factory(controlParams, visParams.useTimeFilter, deps); }); const controls = await Promise.all<RangeControl | ListControl>(controlFactoryPromises); diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index 03a355c604c4d..3ff0c83961e2a 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -2,8 +2,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls toExpression on vis_type if it exists 1`] = `"kibana | kibana_context | test"`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 653542bd8837d..57c58a99f09ea 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -92,15 +92,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { uiState = {}; }); - it('handles input_control_vis function', () => { - const params = { - some: 'nested', - data: { here: true }, - }; - const actual = buildPipelineVisFunction.input_control_vis(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 0c244876ca6a3..29f6ec9b069a7 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -219,9 +219,6 @@ export const prepareDimension = (variable: string, data: any) => { }; export const buildPipelineVisFunction: BuildPipelineVisFunction = { - input_control_vis: (params) => { - return `input_control_vis ${prepareJson('visConfig', params)}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, From b454f2a1c2f9388be752eda826d470d698d0c094 Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Wed, 2 Dec 2020 09:26:22 +0100 Subject: [PATCH 043/107] migrate away from rest_total_hits_as_int (#84508) --- x-pack/plugins/graph/public/angular/graph_client_workspace.js | 2 +- x-pack/plugins/graph/server/routes/search.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/plugins/graph/public/angular/graph_client_workspace.js index 5cc06bad4c423..785e221b79865 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/angular/graph_client_workspace.js @@ -1187,7 +1187,7 @@ function GraphWorkspace(options) { // Search for connections between the selected nodes. searcher(self.options.indexName, searchReq, function (data) { - const numDocsMatched = data.hits.total; + const numDocsMatched = data.hits.total.value; const buckets = data.aggregations.matrix.buckets; const vertices = nodesForLinking.map(function (existingNode) { return { diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 1bd2861687281..7d05f9ab6888c 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -47,7 +47,7 @@ export function registerSearchRoute({ await esClient.asCurrentUser.search({ index: request.body.index, body: request.body.body, - rest_total_hits_as_int: true, + track_total_hits: true, ignore_throttled: !includeFrozen, }) ).body, From 717a66fc6c41b484bd61071cc183313f3c0c3802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= <alejandro.haro@elastic.co> Date: Wed, 2 Dec 2020 09:26:45 +0100 Subject: [PATCH 044/107] TelemetryCollectionManager: Use X-Pack strategy as an OSS overwrite (#84477) --- src/plugins/telemetry/server/fetcher.ts | 2 - src/plugins/telemetry/server/index.ts | 1 - src/plugins/telemetry/server/plugin.ts | 15 +- .../telemetry_collection/get_local_stats.ts | 2 +- .../server/telemetry_collection/index.ts | 1 - .../register_collection.ts | 14 +- .../server/index.ts | 2 - .../server/plugin.ts | 157 +++++++----------- .../server/types.ts | 74 ++------- .../apis/telemetry/telemetry_local.js | 2 + .../telemetry_collection/get_licenses.ts | 2 +- .../common/index.ts | 8 - .../server/index.ts | 7 +- .../server/plugin.ts | 26 +-- .../get_stats_with_xpack.test.ts.snap | 6 + .../telemetry_collection/get_license.test.ts | 82 +++++++++ .../telemetry_collection/get_license.ts | 45 +++-- .../get_stats_with_xpack.ts | 23 ++- .../server/telemetry_collection/index.ts | 1 + .../apis/telemetry/telemetry_local.js | 3 + 20 files changed, 218 insertions(+), 255 deletions(-) delete mode 100644 x-pack/plugins/telemetry_collection_xpack/common/index.ts create mode 100644 x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts rename src/plugins/telemetry/server/telemetry_collection/get_local_license.ts => x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts (50%) diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index a3649f51577ac..820f2c7c4c4af 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -17,7 +17,6 @@ * under the License. */ -import moment from 'moment'; import { Observable, Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; // @ts-ignore @@ -213,7 +212,6 @@ export class FetcherTask { private async fetchTelemetry() { return await this.telemetryCollectionManager!.getStats({ unencrypted: false, - timestamp: moment().valueOf(), }); } diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index e9887456e2f36..326c87a75b0ea 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -44,7 +44,6 @@ export const plugin = (initializerContext: PluginInitializerContext<TelemetryCon export { constants }; export { getClusterUuids, - getLocalLicense, getLocalStats, TelemetryLocalStats, DATA_TELEMETRY_ID, diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 037f97fb63ac6..f40c38b2cbbd0 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -33,9 +33,7 @@ import { SavedObjectsClient, Plugin, Logger, - IClusterClient, UiSettingsServiceStart, - SavedObjectsServiceStart, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -88,8 +86,6 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl */ private readonly oldUiSettingsHandled$ = new AsyncSubject(); private savedObjectsClient?: ISavedObjectsRepository; - private elasticsearchClient?: IClusterClient; - private savedObjectsService?: SavedObjectsServiceStart; constructor(initializerContext: PluginInitializerContext<TelemetryConfigType>) { this.logger = initializerContext.logger.get(); @@ -109,12 +105,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl const currentKibanaVersion = this.currentKibanaVersion; const config$ = this.config$; const isDev = this.isDev; - registerCollection( - telemetryCollectionManager, - elasticsearch.legacy.client, - () => this.elasticsearchClient, - () => this.savedObjectsService - ); + registerCollection(telemetryCollectionManager); const router = http.createRouter(); registerRoutes({ @@ -138,11 +129,9 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl } public start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsDepsStart) { - const { savedObjects, uiSettings, elasticsearch } = core; + const { savedObjects, uiSettings } = core; const savedObjectsInternalRepository = savedObjects.createInternalRepository(); this.savedObjectsClient = savedObjectsInternalRepository; - this.elasticsearchClient = elasticsearch.client; - this.savedObjectsService = savedObjects; // Not catching nor awaiting these promises because they should never reject this.handleOldUiSettings(uiSettings); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index a3666683a05a1..12245ce62305e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -66,7 +66,7 @@ export type TelemetryLocalStats = ReturnType<typeof handleLocalStats>; * @param {Object} config contains the usageCollection, callCluster (deprecated), the esClient and Saved Objects client scoped to the request or the internal repository, and the kibana request * @param {Object} StatsCollectionContext contains logger and version (string) */ -export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( +export const getLocalStats: StatsGetter<TelemetryLocalStats> = async ( clustersDetails, config, context diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 40cbf0e4caa1d..77894091f6133 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -24,6 +24,5 @@ export { buildDataTelemetryPayload, } from './get_data_telemetry'; export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; -export { getLocalLicense } from './get_local_license'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts index 27ca5ae746512..fac315b01493e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -36,27 +36,17 @@ * under the License. */ -import { ILegacyClusterClient, SavedObjectsServiceStart } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { IClusterClient } from '../../../../../src/core/server'; import { getLocalStats } from './get_local_stats'; import { getClusterUuids } from './get_cluster_stats'; -import { getLocalLicense } from './get_local_license'; export function registerCollection( - telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - esCluster: ILegacyClusterClient, - esClientGetter: () => IClusterClient | undefined, - soServiceGetter: () => SavedObjectsServiceStart | undefined + telemetryCollectionManager: TelemetryCollectionManagerPluginSetup ) { - telemetryCollectionManager.setCollection({ - esCluster, - esClientGetter, - soServiceGetter, + telemetryCollectionManager.setCollectionStrategy({ title: 'local', priority: 0, statsGetter: getLocalStats, clusterDetailsGetter: getClusterUuids, - licenseGetter: getLocalLicense, }); } diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index 36ab64731fe58..de2080059c80b 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -30,13 +30,11 @@ export function plugin(initializerContext: PluginInitializerContext) { export { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, - ESLicense, StatsCollectionConfig, StatsGetter, StatsGetterConfig, StatsCollectionContext, ClusterDetails, ClusterDetailsGetter, - LicenseGetter, UsageStatsPayload, } from './types'; diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index bc33e9fbc82c5..a135f4b115b21 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -26,14 +26,15 @@ import { Logger, IClusterClient, SavedObjectsServiceStart, -} from '../../../core/server'; + ILegacyClusterClient, +} from 'src/core/server'; import { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, BasicStatsPayload, - CollectionConfig, - Collection, + CollectionStrategyConfig, + CollectionStrategy, StatsGetterConfig, StatsCollectionConfig, UsageStatsPayload, @@ -49,9 +50,12 @@ interface TelemetryCollectionPluginsDepsSetup { export class TelemetryCollectionManagerPlugin implements Plugin<TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart> { private readonly logger: Logger; - private readonly collections: Array<Collection<any>> = []; + private collectionStrategy: CollectionStrategy<any> | undefined; private usageGetterMethodPriority = -1; private usageCollection?: UsageCollectionSetup; + private legacyElasticsearchClient?: ILegacyClusterClient; + private elasticsearchClient?: IClusterClient; + private savedObjectsService?: SavedObjectsServiceStart; private readonly isDistributable: boolean; private readonly version: string; @@ -65,7 +69,7 @@ export class TelemetryCollectionManagerPlugin this.usageCollection = usageCollection; return { - setCollection: this.setCollection.bind(this), + setCollectionStrategy: this.setCollectionStrategy.bind(this), getOptInStats: this.getOptInStats.bind(this), getStats: this.getStats.bind(this), areAllCollectorsReady: this.areAllCollectorsReady.bind(this), @@ -73,8 +77,11 @@ export class TelemetryCollectionManagerPlugin } public start(core: CoreStart) { + this.legacyElasticsearchClient = core.elasticsearch.legacy.client; // TODO: Remove when all the collectors have migrated + this.elasticsearchClient = core.elasticsearch.client; + this.savedObjectsService = core.savedObjects; + return { - setCollection: this.setCollection.bind(this), getOptInStats: this.getOptInStats.bind(this), getStats: this.getStats.bind(this), areAllCollectorsReady: this.areAllCollectorsReady.bind(this), @@ -83,19 +90,10 @@ export class TelemetryCollectionManagerPlugin public stop() {} - private setCollection<CustomContext extends Record<string, any>, T extends BasicStatsPayload>( - collectionConfig: CollectionConfig<CustomContext, T> + private setCollectionStrategy<T extends BasicStatsPayload>( + collectionConfig: CollectionStrategyConfig<T> ) { - const { - title, - priority, - esCluster, - esClientGetter, - soServiceGetter, - statsGetter, - clusterDetailsGetter, - licenseGetter, - } = collectionConfig; + const { title, priority, statsGetter, clusterDetailsGetter } = collectionConfig; if (typeof priority !== 'number') { throw new Error('priority must be set.'); @@ -108,78 +106,58 @@ export class TelemetryCollectionManagerPlugin if (!statsGetter) { throw Error('Stats getter method not set.'); } - if (!esCluster) { - throw Error('esCluster name must be set for the getCluster method.'); - } - if (!esClientGetter) { - throw Error('esClientGetter method not set.'); - } - if (!soServiceGetter) { - throw Error('soServiceGetter method not set.'); - } if (!clusterDetailsGetter) { throw Error('Cluster UUIds method is not set.'); } - if (!licenseGetter) { - throw Error('License getter method not set.'); - } - this.collections.unshift({ - licenseGetter, - statsGetter, - clusterDetailsGetter, - esCluster, - title, - esClientGetter, - soServiceGetter, - }); + this.logger.debug(`Setting ${title} as the telemetry collection strategy`); + + // Overwrite the collection strategy + this.collectionStrategy = collectionConfig; this.usageGetterMethodPriority = priority; } } + /** + * Returns the context to provide to the Collection Strategies. + * It may return undefined if the ES and SO clients are not initialised yet. + * @param config {@link StatsGetterConfig} + * @param usageCollection {@link UsageCollectionSetup} + * @private + */ private getStatsCollectionConfig( config: StatsGetterConfig, - collection: Collection, - collectionEsClient: IClusterClient, - collectionSoService: SavedObjectsServiceStart, usageCollection: UsageCollectionSetup - ): StatsCollectionConfig { - const { request } = config; - + ): StatsCollectionConfig | undefined { const callCluster = config.unencrypted - ? collection.esCluster.asScoped(request).callAsCurrentUser - : collection.esCluster.callAsInternalUser; + ? this.legacyElasticsearchClient?.asScoped(config.request).callAsCurrentUser + : this.legacyElasticsearchClient?.callAsInternalUser; // Scope the new elasticsearch Client appropriately and pass to the stats collection config const esClient = config.unencrypted - ? collectionEsClient.asScoped(config.request).asCurrentUser - : collectionEsClient.asInternalUser; + ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser + : this.elasticsearchClient?.asInternalUser; // Scope the saved objects client appropriately and pass to the stats collection config const soClient = config.unencrypted - ? collectionSoService.getScopedClient(config.request) - : collectionSoService.createInternalRepository(); + ? this.savedObjectsService?.getScopedClient(config.request) + : this.savedObjectsService?.createInternalRepository(); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted - const kibanaRequest = config.unencrypted ? request : void 0; + const kibanaRequest = config.unencrypted ? config.request : void 0; - return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; + if (callCluster && esClient && soClient) { + return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; + } } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { if (!this.usageCollection) { return []; } - for (const collection of this.collections) { - // first fetch the client and make sure it's not undefined. - const collectionEsClient = collection.esClientGetter(); - const collectionSoService = collection.soServiceGetter(); - if (collectionEsClient !== undefined && collectionSoService !== undefined) { - const statsCollectionConfig = this.getStatsCollectionConfig( - config, - collection, - collectionEsClient, - collectionSoService, - this.usageCollection - ); + const collection = this.collectionStrategy; + if (collection) { + // Build the context (clients and others) to send to the CollectionStrategies + const statsCollectionConfig = this.getStatsCollectionConfig(config, this.usageCollection); + if (statsCollectionConfig) { try { const optInStats = await this.getOptInStatsForCollection( collection, @@ -194,8 +172,9 @@ export class TelemetryCollectionManagerPlugin return encryptTelemetry(optInStats, { useProdKey: this.isDistributable }); } } catch (err) { - this.logger.debug(`Failed to collect any opt in stats with registered collections.`); - // swallow error to try next collection; + this.logger.debug( + `Failed to collect any opt in stats with collection ${collection.title}.` + ); } } } @@ -203,19 +182,18 @@ export class TelemetryCollectionManagerPlugin return []; } - private areAllCollectorsReady = async () => { + private async areAllCollectorsReady() { return await this.usageCollection?.areAllCollectorsReady(); - }; + } private getOptInStatsForCollection = async ( - collection: Collection, + collection: CollectionStrategy, optInStatus: boolean, statsCollectionConfig: StatsCollectionConfig ) => { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), version: this.version, - ...collection.customContext, }; const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig, context); @@ -229,17 +207,11 @@ export class TelemetryCollectionManagerPlugin if (!this.usageCollection) { return []; } - for (const collection of this.collections) { - const collectionEsClient = collection.esClientGetter(); - const collectionSavedObjectsService = collection.soServiceGetter(); - if (collectionEsClient !== undefined && collectionSavedObjectsService !== undefined) { - const statsCollectionConfig = this.getStatsCollectionConfig( - config, - collection, - collectionEsClient, - collectionSavedObjectsService, - this.usageCollection - ); + const collection = this.collectionStrategy; + if (collection) { + // Build the context (clients and others) to send to the CollectionStrategies + const statsCollectionConfig = this.getStatsCollectionConfig(config, this.usageCollection); + if (statsCollectionConfig) { try { const usageData = await this.getUsageForCollection(collection, statsCollectionConfig); if (usageData.length) { @@ -256,7 +228,6 @@ export class TelemetryCollectionManagerPlugin this.logger.debug( `Failed to collect any usage with registered collection ${collection.title}.` ); - // swallow error to try next collection; } } } @@ -265,34 +236,24 @@ export class TelemetryCollectionManagerPlugin } private async getUsageForCollection( - collection: Collection, + collection: CollectionStrategy, statsCollectionConfig: StatsCollectionConfig ): Promise<UsageStatsPayload[]> { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), version: this.version, - ...collection.customContext, }; const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig, context); if (clustersDetails.length === 0) { - // don't bother doing a further lookup, try next collection. + // don't bother doing a further lookup. return []; } - const [stats, licenses] = await Promise.all([ - collection.statsGetter(clustersDetails, statsCollectionConfig, context), - collection.licenseGetter(clustersDetails, statsCollectionConfig, context), - ]); + const stats = await collection.statsGetter(clustersDetails, statsCollectionConfig, context); - return stats.map((stat) => { - const license = licenses[stat.cluster_uuid]; - return { - collectionSource: collection.title, - ...(license ? { license } : {}), - ...stat, - }; - }); + // Add the `collectionSource` to the resulting payload + return stats.map((stat) => ({ collectionSource: collection.title, ...stat })); } } diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index a6cf1a9e5aaf9..05641d5064593 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -19,21 +19,18 @@ import { LegacyAPICaller, + ElasticsearchClient, Logger, KibanaRequest, - ILegacyClusterClient, - IClusterClient, - SavedObjectsServiceStart, SavedObjectsClientContract, ISavedObjectsRepository, -} from 'kibana/server'; +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ElasticsearchClient } from '../../../../src/core/server'; import { TelemetryCollectionManagerPlugin } from './plugin'; export interface TelemetryCollectionManagerPluginSetup { - setCollection: <CustomContext extends Record<string, any>, T extends BasicStatsPayload>( - collectionConfig: CollectionConfig<CustomContext, T> + setCollectionStrategy: <T extends BasicStatsPayload>( + collectionConfig: CollectionStrategyConfig<T> ) => void; getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats']; getStats: TelemetryCollectionManagerPlugin['getStats']; @@ -41,9 +38,6 @@ export interface TelemetryCollectionManagerPluginSetup { } export interface TelemetryCollectionManagerPluginStart { - setCollection: <CustomContext extends Record<string, any>, T extends BasicStatsPayload>( - collectionConfig: CollectionConfig<CustomContext, T> - ) => void; getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats']; getStats: TelemetryCollectionManagerPlugin['getStats']; areAllCollectorsReady: TelemetryCollectionManagerPlugin['areAllCollectorsReady']; @@ -91,74 +85,34 @@ export interface BasicStatsPayload { } export interface UsageStatsPayload extends BasicStatsPayload { - license?: ESLicense; collectionSource: string; } -// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html -export interface ESLicense { - status: string; - uid: string; - type: string; - issue_date: string; - issue_date_in_millis: number; - expiry_date: string; - expirty_date_in_millis: number; - max_nodes: number; - issued_to: string; - issuer: string; - start_date_in_millis: number; -} - export interface StatsCollectionContext { logger: Logger | Console; version: string; } export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; -export type ClusterDetailsGetter<CustomContext extends Record<string, any> = {}> = ( +export type ClusterDetailsGetter = ( config: StatsCollectionConfig, - context: StatsCollectionContext & CustomContext + context: StatsCollectionContext ) => Promise<ClusterDetails[]>; -export type StatsGetter< - CustomContext extends Record<string, any> = {}, - T extends BasicStatsPayload = BasicStatsPayload -> = ( +export type StatsGetter<T extends BasicStatsPayload = BasicStatsPayload> = ( clustersDetails: ClusterDetails[], config: StatsCollectionConfig, - context: StatsCollectionContext & CustomContext + context: StatsCollectionContext ) => Promise<T[]>; -export type LicenseGetter<CustomContext extends Record<string, any> = {}> = ( - clustersDetails: ClusterDetails[], - config: StatsCollectionConfig, - context: StatsCollectionContext & CustomContext -) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>; -export interface CollectionConfig< - CustomContext extends Record<string, any> = {}, - T extends BasicStatsPayload = BasicStatsPayload -> { +export interface CollectionStrategyConfig<T extends BasicStatsPayload = BasicStatsPayload> { title: string; priority: number; - esCluster: ILegacyClusterClient; - esClientGetter: () => IClusterClient | undefined; // --> by now we know that the client getter will return the IClusterClient but we assure that through a code check - soServiceGetter: () => SavedObjectsServiceStart | undefined; // --> by now we know that the service getter will return the SavedObjectsServiceStart but we assure that through a code check - statsGetter: StatsGetter<CustomContext, T>; - clusterDetailsGetter: ClusterDetailsGetter<CustomContext>; - licenseGetter: LicenseGetter<CustomContext>; - customContext?: CustomContext; + statsGetter: StatsGetter<T>; + clusterDetailsGetter: ClusterDetailsGetter; } -export interface Collection< - CustomContext extends Record<string, any> = {}, - T extends BasicStatsPayload = BasicStatsPayload -> { - customContext?: CustomContext; - statsGetter: StatsGetter<CustomContext, T>; - licenseGetter: LicenseGetter<CustomContext>; - clusterDetailsGetter: ClusterDetailsGetter<CustomContext>; - esCluster: ILegacyClusterClient; - esClientGetter: () => IClusterClient | undefined; // the collection could still return undefined for the es client getter. - soServiceGetter: () => SavedObjectsServiceStart | undefined; // the collection could still return undefined for the Saved Objects Service getter. +export interface CollectionStrategy<T extends BasicStatsPayload = BasicStatsPayload> { + statsGetter: StatsGetter<T>; + clusterDetailsGetter: ClusterDetailsGetter; title: string; } diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index b3d34d5910fc3..a025a65779e9c 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -62,6 +62,8 @@ export default function ({ getService }) { expect(body.length).to.be(1); const stats = body[0]; expect(stats.collection).to.be('local'); + expect(stats.collectionSource).to.be('local'); + expect(stats.license).to.be.undefined; // OSS cannot get the license expect(stats.stack_stats.kibana.count).to.be.a('number'); expect(stats.stack_stats.kibana.indices).to.be.a('number'); expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts index 7b1b877c51278..792389485164d 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -5,8 +5,8 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ESLicense } from 'src/plugins/telemetry_collection_manager/server'; import { LegacyAPICaller } from 'kibana/server'; +import { ESLicense } from '../../../telemetry_collection_xpack/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** diff --git a/x-pack/plugins/telemetry_collection_xpack/common/index.ts b/x-pack/plugins/telemetry_collection_xpack/common/index.ts deleted file mode 100644 index 2b08ebe2e7bbf..0000000000000 --- a/x-pack/plugins/telemetry_collection_xpack/common/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const PLUGIN_ID = 'telemetryCollectionXpack'; -export const PLUGIN_NAME = 'telemetry_collection_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index 249d16c331c39..de39089fe0e03 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'kibana/server'; import { TelemetryCollectionXpackPlugin } from './plugin'; +export { ESLicense } from './telemetry_collection'; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. -export function plugin(initializerContext: PluginInitializerContext) { - return new TelemetryCollectionXpackPlugin(initializerContext); +export function plugin() { + return new TelemetryCollectionXpackPlugin(); } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index 524b4c5616c73..e6d72f5813163 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -4,16 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - IClusterClient, - SavedObjectsServiceStart, -} from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { getClusterUuids, getLocalLicense } from '../../../../src/plugins/telemetry/server'; +import { getClusterUuids } from '../../../../src/plugins/telemetry/server'; import { getStatsWithXpack } from './telemetry_collection'; interface TelemetryCollectionXpackDepsSetup { @@ -21,25 +14,16 @@ interface TelemetryCollectionXpackDepsSetup { } export class TelemetryCollectionXpackPlugin implements Plugin { - private elasticsearchClient?: IClusterClient; - private savedObjectsService?: SavedObjectsServiceStart; - constructor(initializerContext: PluginInitializerContext) {} + constructor() {} public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { - telemetryCollectionManager.setCollection({ - esCluster: core.elasticsearch.legacy.client, - esClientGetter: () => this.elasticsearchClient, - soServiceGetter: () => this.savedObjectsService, + telemetryCollectionManager.setCollectionStrategy({ title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, clusterDetailsGetter: getClusterUuids, - licenseGetter: getLocalLicense, }); } - public start(core: CoreStart) { - this.elasticsearchClient = core.elasticsearch.client; - this.savedObjectsService = core.savedObjects; - } + public start(core: CoreStart) {} } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index b68186c0c343d..836b5276615ef 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -104,6 +104,9 @@ Object { }, "cluster_uuid": "test", "collection": "local", + "license": Object { + "type": "basic", + }, "stack_stats": Object { "data": Array [], "kibana": Object { @@ -183,6 +186,9 @@ Object { }, "cluster_uuid": "test", "collection": "local", + "license": Object { + "type": "basic", + }, "stack_stats": Object { "data": Array [], "kibana": Object { diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts new file mode 100644 index 0000000000000..c5c6832aa84ac --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { getLicenseFromLocalOrMaster } from './get_license'; + +describe('getLicenseFromLocalOrMaster', () => { + test('return an undefined license if it fails to get the license on the first attempt and it does not have a cached license yet', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The local fetch fails + esClient.license.get.mockRejectedValue(new Error('Something went terribly wrong')); + + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toBeUndefined(); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(1); + }); + + test('returns the license it fetches from Elasticsearch', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The local fetch succeeds + esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } } as any); + + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toStrictEqual({ type: 'basic' }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(1); + }); + + test('after the first successful attempt, if the local request fails, it will try with the master request (failed case)', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const error = new Error('Something went terribly wrong'); + // The requests fail with an error + esClient.license.get.mockRejectedValue(error); + + await expect(getLicenseFromLocalOrMaster(esClient)).rejects.toStrictEqual(error); + + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(2); + }); + + test('after the first successful attempt, if the local request fails, it will try with the master request (success case)', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The local fetch fails + esClient.license.get.mockRejectedValueOnce(new Error('Something went terribly wrong')); + // The master fetch succeeds + esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } } as any); + + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toStrictEqual({ type: 'basic' }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(2); + }); + + test('after the first successful attempt, if the local request fails, it will try with the master request (clearing cached license)', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The requests fail with 400 + esClient.license.get.mockRejectedValue({ statusCode: 400 }); + + // First attempt goes through 2 requests: local and master + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toBeUndefined(); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(2); + + // Now the cached license is cleared, next request only goes for local and gives up when failed + esClient.license.get.mockClear(); + await expect(getLicenseFromLocalOrMaster(esClient)).resolves.toBeUndefined(); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts similarity index 50% rename from src/plugins/telemetry/server/telemetry_collection/get_local_license.ts rename to x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts index 879416cda62fc..9ffbf5d1bf6d7 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts @@ -1,29 +1,30 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 { ESLicense, LicenseGetter } from 'src/plugins/telemetry_collection_manager/server'; import { ElasticsearchClient } from 'src/core/server'; +// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html +export interface ESLicense { + status: string; + uid: string; + type: string; + issue_date: string; + issue_date_in_millis: number; + expiry_date: string; + expirty_date_in_millis: number; + max_nodes: number; + issued_to: string; + issuer: string; + start_date_in_millis: number; +} + let cachedLicense: ESLicense | undefined; async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { - const { body } = await esClient.license.get({ + const { body } = await esClient.license.get<{ license: ESLicense }>({ local, // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. accept_enterprise: true, @@ -39,7 +40,7 @@ async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { * * In OSS we'll get a 400 response using the new elasticsearch client. */ -async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { +export async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { // Fetching the local license is cheaper than getting it from the master node and good enough const { license } = await fetchLicense(esClient, true).catch(async (err) => { if (cachedLicense) { @@ -64,9 +65,3 @@ async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { } return license; } - -export const getLocalLicense: LicenseGetter = async (clustersDetails, { esClient }) => { - const license = await getLicenseFromLocalOrMaster(esClient); - // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. - return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); -}; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts index c0e55274b08df..bf1e7c3aaae17 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts @@ -4,33 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ +import { StatsGetter } from 'src/plugins/telemetry_collection_manager/server'; import { TelemetryLocalStats, getLocalStats } from '../../../../../src/plugins/telemetry/server'; -import { StatsGetter } from '../../../../../src/plugins/telemetry_collection_manager/server'; import { getXPackUsage } from './get_xpack'; +import { ESLicense, getLicenseFromLocalOrMaster } from './get_license'; export type TelemetryAggregatedStats = TelemetryLocalStats & { stack_stats: { xpack?: object }; + license?: ESLicense; }; -export const getStatsWithXpack: StatsGetter<{}, TelemetryAggregatedStats> = async function ( +export const getStatsWithXpack: StatsGetter<TelemetryAggregatedStats> = async function ( clustersDetails, config, context ) { const { esClient } = config; - const clustersLocalStats = await getLocalStats(clustersDetails, config, context); - const xpack = await getXPackUsage(esClient).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. + const [clustersLocalStats, license, xpack] = await Promise.all([ + getLocalStats(clustersDetails, config, context), + getLicenseFromLocalOrMaster(esClient), + getXPackUsage(esClient).catch(() => undefined), // We want to still report something (and do not lose the license) even when this method fails. + ]); return clustersLocalStats .map((localStats) => { + const localStatsWithLicense: TelemetryAggregatedStats = { + ...localStats, + ...(license && { license }), + }; if (xpack) { return { - ...localStats, - stack_stats: { ...localStats.stack_stats, xpack }, + ...localStatsWithLicense, + stack_stats: { ...localStatsWithLicense.stack_stats, xpack }, }; } - return localStats; + return localStatsWithLicense; }) .reduce((acc, stats) => { // Concatenate the telemetry reported via monitoring as additional payloads instead of reporting it inside of stack_stats.kibana.plugins.monitoringTelemetry diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index 553f8dc0c4188..bcd011ae750a6 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js index 6a5a7db4d2560..e7014ff92d9b9 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -55,6 +55,9 @@ export default function ({ getService }) { const stats = body[0]; expect(stats.collection).to.be('local'); + expect(stats.collectionSource).to.be('local_xpack'); + + // License should exist in X-Pack expect(stats.license.issuer).to.be.a('string'); expect(stats.license.status).to.be('active'); From 8981d0e9e23d96592a7ceaddfba735639aa620a5 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin <aleh.zasypkin@gmail.com> Date: Wed, 2 Dec 2020 09:32:49 +0100 Subject: [PATCH 045/107] Make it possible to use Kibana anonymous authentication provider with ES anonymous access. (#84074) --- .../providers/anonymous.test.ts | 74 +++++++++++++------ .../authentication/providers/anonymous.ts | 51 ++++++++++--- x-pack/plugins/security/server/config.test.ts | 54 ++++++++++++-- x-pack/plugins/security/server/config.ts | 1 + .../anonymous_es_anonymous.config.ts | 40 ++++++++++ .../tests/anonymous/login.ts | 28 ++++--- 6 files changed, 200 insertions(+), 48 deletions(-) create mode 100644 x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts index c296cb9c8e94d..9674181e18750 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts @@ -29,32 +29,48 @@ function expectAuthenticateCall( expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); } +enum CredentialsType { + Basic = 'Basic', + ApiKey = 'ApiKey', + None = 'ES native anonymous', +} + describe('AnonymousAuthenticationProvider', () => { const user = mockAuthenticatedUser({ authentication_provider: { type: 'anonymous', name: 'anonymous1' }, }); - for (const useBasicCredentials of [true, false]) { - describe(`with ${useBasicCredentials ? '`Basic`' : '`ApiKey`'} credentials`, () => { + for (const credentialsType of [ + CredentialsType.Basic, + CredentialsType.ApiKey, + CredentialsType.None, + ]) { + describe(`with ${credentialsType} credentials`, () => { let provider: AnonymousAuthenticationProvider; let mockOptions: ReturnType<typeof mockAuthenticationProviderOptions>; let authorization: string; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions({ name: 'anonymous1' }); - provider = useBasicCredentials - ? new AnonymousAuthenticationProvider(mockOptions, { - credentials: { username: 'user', password: 'pass' }, - }) - : new AnonymousAuthenticationProvider(mockOptions, { - credentials: { apiKey: 'some-apiKey' }, - }); - authorization = useBasicCredentials - ? new HTTPAuthorizationHeader( + let credentials; + switch (credentialsType) { + case CredentialsType.Basic: + credentials = { username: 'user', password: 'pass' }; + authorization = new HTTPAuthorizationHeader( 'Basic', new BasicHTTPAuthorizationHeaderCredentials('user', 'pass').toString() - ).toString() - : new HTTPAuthorizationHeader('ApiKey', 'some-apiKey').toString(); + ).toString(); + break; + case CredentialsType.ApiKey: + credentials = { apiKey: 'some-apiKey' }; + authorization = new HTTPAuthorizationHeader('ApiKey', 'some-apiKey').toString(); + break; + default: + credentials = 'elasticsearch_anonymous_user' as 'elasticsearch_anonymous_user'; + break; + } + + provider = new AnonymousAuthenticationProvider(mockOptions, { credentials }); }); describe('`login` method', () => { @@ -111,23 +127,29 @@ describe('AnonymousAuthenticationProvider', () => { }); it('does not handle authentication via `authorization` header.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); + const originalAuthorizationHeader = 'Basic credentials'; + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: originalAuthorizationHeader }, + }); await expect(provider.authenticate(request)).resolves.toEqual( AuthenticationResult.notHandled() ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(request.headers.authorization).toBe(authorization); + expect(request.headers.authorization).toBe(originalAuthorizationHeader); }); it('does not handle authentication via `authorization` header even if state exists.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); + const originalAuthorizationHeader = 'Basic credentials'; + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: originalAuthorizationHeader }, + }); await expect(provider.authenticate(request, {})).resolves.toEqual( AuthenticationResult.notHandled() ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(request.headers.authorization).toBe(authorization); + expect(request.headers.authorization).toBe(originalAuthorizationHeader); }); it('succeeds for non-AJAX requests if state is available.', async () => { @@ -191,7 +213,7 @@ describe('AnonymousAuthenticationProvider', () => { expect(request.headers).not.toHaveProperty('authorization'); }); - if (!useBasicCredentials) { + if (credentialsType === CredentialsType.ApiKey) { it('properly handles extended format for the ApiKey credentials', async () => { provider = new AnonymousAuthenticationProvider(mockOptions, { credentials: { apiKey: { id: 'some-id', key: 'some-key' } }, @@ -237,9 +259,19 @@ describe('AnonymousAuthenticationProvider', () => { }); it('`getHTTPAuthenticationScheme` method', () => { - expect(provider.getHTTPAuthenticationScheme()).toBe( - useBasicCredentials ? 'basic' : 'apikey' - ); + let expectedAuthenticationScheme; + switch (credentialsType) { + case CredentialsType.Basic: + expectedAuthenticationScheme = 'basic'; + break; + case CredentialsType.ApiKey: + expectedAuthenticationScheme = 'apikey'; + break; + default: + expectedAuthenticationScheme = null; + break; + } + expect(provider.getHTTPAuthenticationScheme()).toBe(expectedAuthenticationScheme); }); }); } diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.ts index 6f02cce371a41..6d62d3a909e55 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from '../../../../../../src/core/server'; +import { KibanaRequest, LegacyElasticsearchErrorHelpers } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -29,6 +29,11 @@ interface APIKeyCredentials { apiKey: { id: string; key: string } | string; } +/** + * Credentials that imply authentication based on the Elasticsearch native anonymous user. + */ +type ElasticsearchAnonymousUserCredentials = 'elasticsearch_anonymous_user'; + /** * Checks whether current request can initiate a new session. * @param request Request instance. @@ -44,7 +49,10 @@ function canStartNewSession(request: KibanaRequest) { * @param credentials */ function isAPIKeyCredentials( - credentials: UsernameAndPasswordCredentials | APIKeyCredentials + credentials: + | ElasticsearchAnonymousUserCredentials + | APIKeyCredentials + | UsernameAndPasswordCredentials ): credentials is APIKeyCredentials { return !!(credentials as APIKeyCredentials).apiKey; } @@ -59,14 +67,17 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider static readonly type = 'anonymous'; /** - * Defines HTTP authorization header that should be used to authenticate request. + * Defines HTTP authorization header that should be used to authenticate request. It isn't defined + * if provider should rely on Elasticsearch native anonymous access. */ - private readonly httpAuthorizationHeader: HTTPAuthorizationHeader; + private readonly httpAuthorizationHeader?: HTTPAuthorizationHeader; constructor( protected readonly options: Readonly<AuthenticationProviderOptions>, anonymousOptions?: Readonly<{ - credentials?: Readonly<UsernameAndPasswordCredentials | APIKeyCredentials>; + credentials?: Readonly< + ElasticsearchAnonymousUserCredentials | UsernameAndPasswordCredentials | APIKeyCredentials + >; }> ) { super(options); @@ -76,7 +87,11 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider throw new Error('Credentials must be specified'); } - if (isAPIKeyCredentials(credentials)) { + if (credentials === 'elasticsearch_anonymous_user') { + this.logger.debug( + 'Anonymous requests will be authenticated using Elasticsearch native anonymous user.' + ); + } else if (isAPIKeyCredentials(credentials)) { this.logger.debug('Anonymous requests will be authenticated via API key.'); this.httpAuthorizationHeader = new HTTPAuthorizationHeader( 'ApiKey', @@ -155,7 +170,7 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider * HTTP header that provider attaches to all successfully authenticated requests to Elasticsearch. */ public getHTTPAuthenticationScheme() { - return this.httpAuthorizationHeader.scheme.toLowerCase(); + return this.httpAuthorizationHeader?.scheme.toLowerCase() ?? null; } /** @@ -164,7 +179,9 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider * @param state State value previously stored by the provider. */ private async authenticateViaAuthorizationHeader(request: KibanaRequest, state?: unknown) { - const authHeaders = { authorization: this.httpAuthorizationHeader.toString() }; + const authHeaders = this.httpAuthorizationHeader + ? { authorization: this.httpAuthorizationHeader.toString() } + : ({} as Record<string, string>); try { const user = await this.getUser(request, authHeaders); this.logger.debug( @@ -173,7 +190,23 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider // Create session only if it doesn't exist yet, otherwise keep it unchanged. return AuthenticationResult.succeeded(user, { authHeaders, state: state ? undefined : {} }); } catch (err) { - this.logger.debug(`Failed to authenticate request : ${err.message}`); + if (LegacyElasticsearchErrorHelpers.isNotAuthorizedError(err)) { + if (!this.httpAuthorizationHeader) { + this.logger.error( + `Failed to authenticate anonymous request using Elasticsearch reserved anonymous user. Anonymous access may not be properly configured in Elasticsearch: ${err.message}` + ); + } else if (this.httpAuthorizationHeader.scheme.toLowerCase() === 'basic') { + this.logger.error( + `Failed to authenticate anonymous request using provided username/password credentials. The user with the provided username may not exist or the password is wrong: ${err.message}` + ); + } else { + this.logger.error( + `Failed to authenticate anonymous request using provided API key. The key may not exist or expired: ${err.message}` + ); + } + } else { + this.logger.error(`Failed to authenticate request : ${err.message}`); + } return AuthenticationResult.failed(err); } } diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index a306e701e4e8d..f41e721db33a2 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -902,8 +902,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.password]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: expected at least one defined value but got [undefined]" + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.password]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: expected at least one defined value but got [undefined]" `); expect(() => @@ -918,8 +919,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.username]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: expected at least one defined value but got [undefined]" + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.username]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: expected at least one defined value but got [undefined]" `); }); @@ -973,8 +975,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.username]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: types that failed validation: + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.username]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: types that failed validation: - [credentials.apiKey.0.key]: expected value of type [string] but got [undefined] - [credentials.apiKey.1]: expected value of type [string] but got [Object]" `); @@ -993,8 +996,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.username]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: types that failed validation: + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.username]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: types that failed validation: - [credentials.apiKey.0.id]: expected value of type [string] but got [undefined] - [credentials.apiKey.1]: expected value of type [string] but got [Object]" `); @@ -1073,6 +1077,40 @@ describe('config schema', () => { `); }); + it('can be successfully validated with `elasticsearch_anonymous_user` credentials', () => { + expect( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: 'elasticsearch_anonymous_user', + }, + }, + }, + }, + }).authc.providers + ).toMatchInlineSnapshot(` + Object { + "anonymous": Object { + "anonymous1": Object { + "credentials": "elasticsearch_anonymous_user", + "description": "Continue as Guest", + "enabled": true, + "hint": "For anonymous users", + "icon": "globe", + "order": 0, + "session": Object { + "idleTimeout": null, + }, + "showInSelector": true, + }, + }, + } + `); + }); + it('can be successfully validated with session config overrides', () => { expect( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index b46c8dc2178a4..cf9ab3a78ab71 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -150,6 +150,7 @@ const providersConfigSchema = schema.object( }, { credentials: schema.oneOf([ + schema.literal('elasticsearch_anonymous_user'), schema.object({ username: schema.string(), password: schema.string(), diff --git a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts new file mode 100644 index 0000000000000..3fc30c922b742 --- /dev/null +++ b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const anonymousAPITestsConfig = await readConfigFile(require.resolve('./anonymous.config.ts')); + return { + ...anonymousAPITestsConfig.getAll(), + + junit: { + reportName: 'X-Pack Security API Integration Tests (Anonymous with ES anonymous access)', + }, + + esTestCluster: { + ...anonymousAPITestsConfig.get('esTestCluster'), + serverArgs: [ + ...anonymousAPITestsConfig.get('esTestCluster.serverArgs'), + 'xpack.security.authc.anonymous.username=anonymous_user', + 'xpack.security.authc.anonymous.roles=anonymous_role', + ], + }, + + kbnTestServer: { + ...anonymousAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...anonymousAPITestsConfig + .get('kbnTestServer.serverArgs') + .filter((arg: string) => !arg.startsWith('--xpack.security.authc.providers')), + `--xpack.security.authc.providers=${JSON.stringify({ + anonymous: { anonymous1: { order: 0, credentials: 'elasticsearch_anonymous_user' } }, + basic: { basic1: { order: 1 } }, + })}`, + ], + }, + }; +} diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts index e7c876f54ee5a..559dc7cc78036 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/login.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -31,18 +31,24 @@ export default function ({ getService }: FtrProviderContext) { expect(cookie.maxAge).to.be(0); } + const isElasticsearchAnonymousAccessEnabled = (config.get( + 'esTestCluster.serverArgs' + ) as string[]).some((setting) => setting.startsWith('xpack.security.authc.anonymous')); + describe('Anonymous authentication', () => { - before(async () => { - await security.user.create('anonymous_user', { - password: 'changeme', - roles: [], - full_name: 'Guest', + if (!isElasticsearchAnonymousAccessEnabled) { + before(async () => { + await security.user.create('anonymous_user', { + password: 'changeme', + roles: [], + full_name: 'Guest', + }); }); - }); - after(async () => { - await security.user.delete('anonymous_user'); - }); + after(async () => { + await security.user.delete('anonymous_user'); + }); + } it('should reject API requests if client is not authenticated', async () => { await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); @@ -97,7 +103,9 @@ export default function ({ getService }: FtrProviderContext) { expect(user.username).to.eql('anonymous_user'); expect(user.authentication_provider).to.eql({ type: 'anonymous', name: 'anonymous1' }); - expect(user.authentication_type).to.eql('realm'); + expect(user.authentication_type).to.eql( + isElasticsearchAnonymousAccessEnabled ? 'anonymous' : 'realm' + ); // Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud }); From 44c436b8ad8bab89a9dccf53b496cf62a01d26b5 Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Wed, 2 Dec 2020 09:42:42 +0100 Subject: [PATCH 046/107] [Lens] Use index pattern through service instead of reading saved object (#84432) --- .../indexpattern_datasource/datapanel.tsx | 3 +- .../indexpattern_datasource/field_list.tsx | 7 -- x-pack/plugins/lens/server/plugin.tsx | 2 + .../server/routes/existing_fields.test.ts | 30 ++++---- .../lens/server/routes/existing_fields.ts | 71 ++++++++----------- .../plugins/lens/server/routes/field_stats.ts | 3 +- x-pack/plugins/lens/server/routes/index.ts | 3 +- .../plugins/lens/server/routes/telemetry.ts | 3 +- .../apis/lens/existing_fields.ts | 20 ++++++ 9 files changed, 73 insertions(+), 69 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index ad5509dd88bc9..5121714050c68 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -563,7 +563,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ '{availableFields} available {availableFields, plural, one {field} other {fields}}. {emptyFields} empty {emptyFields, plural, one {field} other {fields}}. {metaFields} meta {metaFields, plural, one {field} other {fields}}.', values: { availableFields: fieldGroups.AvailableFields.fields.length, - emptyFields: fieldGroups.EmptyFields.fields.length, + // empty fields can be undefined if there is no existence information to be fetched + emptyFields: fieldGroups.EmptyFields?.fields.length || 0, metaFields: fieldGroups.MetaFields.fields.length, }, })} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index eb7730677d52a..9e89468200e2c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -14,13 +14,6 @@ import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; const PAGINATION_SIZE = 50; -export interface FieldsGroup { - specialFields: IndexPatternField[]; - availableFields: IndexPatternField[]; - emptyFields: IndexPatternField[]; - metaFields: IndexPatternField[]; -} - export type FieldGroups = Record< string, { diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index a8f9bef92349c..5a55b34cbcdff 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -7,6 +7,7 @@ import { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; +import { PluginStart as DataPluginStart } from 'src/plugins/data/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { setupRoutes } from './routes'; import { @@ -23,6 +24,7 @@ export interface PluginSetupContract { export interface PluginStartContract { taskManager?: TaskManagerStartContract; + data: DataPluginStart; } export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index c877e69d7b0dd..0a3e669ba8538 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IndexPattern } from 'src/plugins/data/common'; import { existingFields, Field, buildFieldList } from './existing_fields'; describe('existingFields', () => { @@ -71,25 +72,20 @@ describe('existingFields', () => { describe('buildFieldList', () => { const indexPattern = { - id: '', - type: 'indexpattern', - attributes: { - title: 'testpattern', - type: 'type', - typeMeta: 'typemeta', - fields: JSON.stringify([ - { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, - { name: 'bar' }, - { name: '@bar' }, - { name: 'baz' }, - { name: '_mymeta' }, - ]), - }, - references: [], + title: 'testpattern', + type: 'type', + typeMeta: 'typemeta', + fields: [ + { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, + { name: 'bar' }, + { name: '@bar' }, + { name: 'baz' }, + { name: '_mymeta' }, + ], }; it('supports scripted fields', () => { - const fields = buildFieldList(indexPattern, []); + const fields = buildFieldList((indexPattern as unknown) as IndexPattern, []); expect(fields.find((f) => f.isScript)).toMatchObject({ isScript: true, name: 'foo', @@ -99,7 +95,7 @@ describe('buildFieldList', () => { }); it('supports meta fields', () => { - const fields = buildFieldList(indexPattern, ['_mymeta']); + const fields = buildFieldList((indexPattern as unknown) as IndexPattern, ['_mymeta']); expect(fields.find((f) => f.isMeta)).toMatchObject({ isScript: false, isMeta: true, diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 844c7b16e1eaa..aef8b1b3d7076 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -6,10 +6,12 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient, SavedObject, RequestHandlerContext } from 'src/core/server'; +import { ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; +import { IndexPattern, IndexPatternsService } from 'src/plugins/data/common'; import { BASE_API_URL } from '../../common'; -import { IndexPatternAttributes, UI_SETTINGS } from '../../../../../src/plugins/data/server'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/server'; +import { PluginStartContract } from '../plugin'; export function isBoomError(error: { isBoom?: boolean }): error is Boom { return error.isBoom === true; @@ -28,7 +30,7 @@ export interface Field { script?: string; } -export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { +export async function existingFieldsRoute(setup: CoreSetup<PluginStartContract>, logger: Logger) { const router = setup.http.createRouter(); router.post( @@ -47,11 +49,18 @@ export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { }, }, async (context, req, res) => { + const [{ savedObjects, elasticsearch }, { data }] = await setup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const esClient = elasticsearch.client.asScoped(req).asCurrentUser; try { return res.ok({ body: await fetchFieldExistence({ ...req.params, ...req.body, + indexPatternsService: await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + esClient + ), context, }), }); @@ -80,6 +89,7 @@ export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { async function fetchFieldExistence({ context, indexPatternId, + indexPatternsService, dslQuery = { match_all: {} }, fromDate, toDate, @@ -87,16 +97,14 @@ async function fetchFieldExistence({ }: { indexPatternId: string; context: RequestHandlerContext; + indexPatternsService: IndexPatternsService; dslQuery: object; fromDate?: string; toDate?: string; timeFieldName?: string; }) { const metaFields: string[] = await context.core.uiSettings.client.get(UI_SETTINGS.META_FIELDS); - const { indexPattern, indexPatternTitle } = await fetchIndexPatternDefinition( - indexPatternId, - context - ); + const indexPattern = await indexPatternsService.get(indexPatternId); const fields = buildFieldList(indexPattern, metaFields); const docs = await fetchIndexPatternStats({ @@ -104,51 +112,32 @@ async function fetchFieldExistence({ toDate, dslQuery, client: context.core.elasticsearch.legacy.client, - index: indexPatternTitle, - timeFieldName: timeFieldName || indexPattern.attributes.timeFieldName, + index: indexPattern.title, + timeFieldName: timeFieldName || indexPattern.timeFieldName, fields, }); return { - indexPatternTitle, + indexPatternTitle: indexPattern.title, existingFieldNames: existingFields(docs, fields), }; } -async function fetchIndexPatternDefinition(indexPatternId: string, context: RequestHandlerContext) { - const savedObjectsClient = context.core.savedObjects.client; - const indexPattern = await savedObjectsClient.get<IndexPatternAttributes>( - 'index-pattern', - indexPatternId - ); - const indexPatternTitle = indexPattern.attributes.title; - - return { - indexPattern, - indexPatternTitle, - }; -} - /** * Exported only for unit tests. */ -export function buildFieldList( - indexPattern: SavedObject<IndexPatternAttributes>, - metaFields: string[] -): Field[] { - return JSON.parse(indexPattern.attributes.fields).map( - (field: { name: string; lang: string; scripted?: boolean; script?: string }) => { - return { - name: field.name, - isScript: !!field.scripted, - lang: field.lang, - script: field.script, - // id is a special case - it doesn't show up in the meta field list, - // but as it's not part of source, it has to be handled separately. - isMeta: metaFields.includes(field.name) || field.name === '_id', - }; - } - ); +export function buildFieldList(indexPattern: IndexPattern, metaFields: string[]): Field[] { + return indexPattern.fields.map((field) => { + return { + name: field.name, + isScript: !!field.scripted, + lang: field.lang, + script: field.script, + // id is a special case - it doesn't show up in the meta field list, + // but as it's not part of source, it has to be handled separately. + isMeta: metaFields.includes(field.name) || field.name === '_id', + }; + }); } async function fetchIndexPatternStats({ diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 29e2416b74618..e0f1e05ed970d 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -11,10 +11,11 @@ import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; +import { PluginStartContract } from '../plugin'; const SHARD_SIZE = 5000; -export async function initFieldsRoute(setup: CoreSetup) { +export async function initFieldsRoute(setup: CoreSetup<PluginStartContract>) { const router = setup.http.createRouter(); router.post( { diff --git a/x-pack/plugins/lens/server/routes/index.ts b/x-pack/plugins/lens/server/routes/index.ts index 01018d8cd7fe5..b1d7e7ca8bc17 100644 --- a/x-pack/plugins/lens/server/routes/index.ts +++ b/x-pack/plugins/lens/server/routes/index.ts @@ -5,11 +5,12 @@ */ import { CoreSetup, Logger } from 'src/core/server'; +import { PluginStartContract } from '../plugin'; import { existingFieldsRoute } from './existing_fields'; import { initFieldsRoute } from './field_stats'; import { initLensUsageRoute } from './telemetry'; -export function setupRoutes(setup: CoreSetup, logger: Logger) { +export function setupRoutes(setup: CoreSetup<PluginStartContract>, logger: Logger) { existingFieldsRoute(setup, logger); initFieldsRoute(setup); initLensUsageRoute(setup); diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index 306c631cd78a7..820e32509923e 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -8,10 +8,11 @@ import Boom from '@hapi/boom'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { BASE_API_URL } from '../../common'; +import { PluginStartContract } from '../plugin'; // This route is responsible for taking a batch of click events from the browser // and writing them to saved objects -export async function initLensUsageRoute(setup: CoreSetup) { +export async function initLensUsageRoute(setup: CoreSetup<PluginStartContract>) { const router = setup.http.createRouter(); router.post( { diff --git a/x-pack/test/api_integration/apis/lens/existing_fields.ts b/x-pack/test/api_integration/apis/lens/existing_fields.ts index 08806df380f38..6eddaac50fda5 100644 --- a/x-pack/test/api_integration/apis/lens/existing_fields.ts +++ b/x-pack/test/api_integration/apis/lens/existing_fields.ts @@ -102,26 +102,46 @@ const metricBeatData = [ '_id', '_index', 'agent.ephemeral_id', + 'agent.ephemeral_id.keyword', 'agent.hostname', + 'agent.hostname.keyword', 'agent.id', + 'agent.id.keyword', 'agent.type', + 'agent.type.keyword', 'agent.version', + 'agent.version.keyword', 'ecs.version', + 'ecs.version.keyword', 'event.dataset', + 'event.dataset.keyword', 'event.duration', 'event.module', + 'event.module.keyword', 'host.architecture', + 'host.architecture.keyword', 'host.hostname', + 'host.hostname.keyword', 'host.id', + 'host.id.keyword', 'host.name', + 'host.name.keyword', 'host.os.build', + 'host.os.build.keyword', 'host.os.family', + 'host.os.family.keyword', 'host.os.kernel', + 'host.os.kernel.keyword', 'host.os.name', + 'host.os.name.keyword', 'host.os.platform', + 'host.os.platform.keyword', 'host.os.version', + 'host.os.version.keyword', 'metricset.name', + 'metricset.name.keyword', 'service.type', + 'service.type.keyword', 'system.cpu.cores', 'system.cpu.idle.pct', 'system.cpu.iowait.pct', From 30f8e41d45a88c9e1289af1548ac106625f7732a Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Wed, 2 Dec 2020 10:06:25 +0100 Subject: [PATCH 047/107] [Lens] Show color in flyout instead of auto (#84532) --- .../xy_visualization/color_assignment.ts | 51 +++++++++++- .../public/xy_visualization/visualization.tsx | 65 ++------------- .../xy_visualization/xy_config_panel.test.tsx | 83 +++++++++++++++++++ .../xy_visualization/xy_config_panel.tsx | 45 ++++++++-- 4 files changed, 180 insertions(+), 64 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 68c47e11acfc0..e5764eaf0e8c0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -5,9 +5,11 @@ */ import { uniq, mapValues } from 'lodash'; -import { PaletteOutput } from 'src/plugins/charts/public'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { Datatable } from 'src/plugins/expressions'; -import { FormatFactory } from '../types'; +import { AccessorConfig, FormatFactory, FramePublicAPI } from '../types'; +import { getColumnToLabelMap } from './state_helpers'; +import { LayerConfig } from './types'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; @@ -87,3 +89,48 @@ export function getColorAssignments( }; }); } + +export function getAccessorColorConfig( + colorAssignments: ColorAssignments, + frame: FramePublicAPI, + layer: LayerConfig, + sortedAccessors: string[], + paletteService: PaletteRegistry +): AccessorConfig[] { + const layerContainsSplits = Boolean(layer.splitAccessor); + const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; + const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; + return sortedAccessors.map((accessor) => { + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); + if (layerContainsSplits) { + return { + columnId: accessor as string, + triggerIcon: 'disabled', + }; + } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); + const rank = colorAssignments[currentPalette.name].getRank( + layer, + columnToLabel[accessor] || accessor, + accessor + ); + const customColor = + currentYConfig?.color || + paletteService.get(currentPalette.name).getColor( + [ + { + name: columnToLabel[accessor] || accessor, + rankAtDepth: rank, + totalSeriesAtDepth: totalSeriesCount, + }, + ], + { maxDepth: 1, totalSeries: totalSeriesCount }, + currentPalette.params + ); + return { + columnId: accessor as string, + triggerIcon: customColor ? 'color' : 'disabled', + color: customColor ? customColor : undefined, + }; + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 872eb179e6a5c..f0dcaf589b1c4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -10,24 +10,18 @@ import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { PaletteRegistry } from 'src/plugins/charts/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; -import { - Visualization, - OperationMetadata, - VisualizationType, - AccessorConfig, - FramePublicAPI, -} from '../types'; +import { Visualization, OperationMetadata, VisualizationType, AccessorConfig } from '../types'; import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; -import { getColumnToLabelMap, isHorizontalChart } from './state_helpers'; +import { isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; -import { ColorAssignments, getColorAssignments } from './color_assignment'; +import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -328,7 +322,11 @@ export const getXyVisualization = ({ renderDimensionEditor(domElement, props) { render( <I18nProvider> - <DimensionEditor {...props} /> + <DimensionEditor + {...props} + formatFactory={data.fieldFormats.deserialize} + paletteService={paletteService} + /> </I18nProvider>, domElement ); @@ -375,51 +373,6 @@ export const getXyVisualization = ({ }, }); -function getAccessorColorConfig( - colorAssignments: ColorAssignments, - frame: FramePublicAPI, - layer: LayerConfig, - sortedAccessors: string[], - paletteService: PaletteRegistry -): AccessorConfig[] { - const layerContainsSplits = Boolean(layer.splitAccessor); - const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; - const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; - return sortedAccessors.map((accessor) => { - const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); - if (layerContainsSplits) { - return { - columnId: accessor as string, - triggerIcon: 'disabled', - }; - } - const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); - const rank = colorAssignments[currentPalette.name].getRank( - layer, - columnToLabel[accessor] || accessor, - accessor - ); - const customColor = - currentYConfig?.color || - paletteService.get(currentPalette.name).getColor( - [ - { - name: columnToLabel[accessor] || accessor, - rankAtDepth: rank, - totalSeriesAtDepth: totalSeriesCount, - }, - ], - { maxDepth: 1, totalSeries: totalSeriesCount }, - currentPalette.params - ); - return { - columnId: accessor as string, - triggerIcon: customColor ? 'color' : 'disabled', - color: customColor ? customColor : undefined, - }; - }); -} - function validateLayersForDimension( dimension: string, layers: LayerConfig[], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 99fbfa058a2de..7b84b990f963a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -14,6 +14,8 @@ import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { EuiColorPicker } from '@elastic/eui'; describe('XY Config panels', () => { let frame: FramePublicAPI; @@ -322,6 +324,8 @@ describe('XY Config panels', () => { accessor="bar" groupId="left" state={{ ...state, layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }] }} + formatFactory={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} /> ); @@ -343,6 +347,8 @@ describe('XY Config panels', () => { accessor="bar" groupId="left" state={state} + formatFactory={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} /> ); @@ -353,5 +359,82 @@ describe('XY Config panels', () => { expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Left', 'Right']); }); + + test('sets the color of a dimension to the color from palette service if not set explicitly', () => { + const state = testState(); + const component = mount( + <DimensionEditor + layerId={state.layers[0].layerId} + frame={{ + ...frame, + activeData: { + first: { + type: 'datatable', + columns: [], + rows: [{ bar: 123 }], + }, + }, + }} + setState={jest.fn()} + accessor="bar" + groupId="left" + state={{ + ...state, + layers: [ + { + seriesType: 'bar', + layerId: 'first', + splitAccessor: undefined, + xAccessor: 'foo', + accessors: ['bar'], + }, + ], + }} + formatFactory={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} + /> + ); + + expect(component.find(EuiColorPicker).prop('color')).toEqual('black'); + }); + + test('uses the overwrite color if set', () => { + const state = testState(); + const component = mount( + <DimensionEditor + layerId={state.layers[0].layerId} + frame={{ + ...frame, + activeData: { + first: { + type: 'datatable', + columns: [], + rows: [{ bar: 123 }], + }, + }, + }} + setState={jest.fn()} + accessor="bar" + groupId="left" + state={{ + ...state, + layers: [ + { + seriesType: 'bar', + layerId: 'first', + splitAccessor: undefined, + xAccessor: 'foo', + accessors: ['bar'], + yConfig: [{ forAccessor: 'bar', color: 'red' }], + }, + ], + }} + formatFactory={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} + /> + ); + + expect(component.find(EuiColorPicker).prop('color')).toEqual('red'); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index a22530c5743b4..cd8a5993d3ecb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -5,7 +5,7 @@ */ import './xy_config_panel.scss'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; import { debounce } from 'lodash'; @@ -22,10 +22,12 @@ import { EuiToolTip, EuiIcon, } from '@elastic/eui'; +import { PaletteRegistry } from 'src/plugins/charts/public'; import { VisualizationLayerWidgetProps, VisualizationToolbarProps, VisualizationDimensionEditorProps, + FormatFactory, } from '../types'; import { State, @@ -48,6 +50,7 @@ import { AxisSettingsPopover } from './axis_settings_popover'; import { TooltipWrapper } from './tooltip_wrapper'; import { getAxesConfiguration } from './axes_configuration'; import { PalettePicker } from '../shared_components'; +import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; type UnwrapArray<T> = T extends Array<infer P> ? P : T; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; @@ -445,7 +448,12 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) { } const idPrefix = htmlIdGenerator()(); -export function DimensionEditor(props: VisualizationDimensionEditorProps<State>) { +export function DimensionEditor( + props: VisualizationDimensionEditorProps<State> & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; + } +) { const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; @@ -556,12 +564,37 @@ const ColorPicker = ({ setState, layerId, accessor, -}: VisualizationDimensionEditorProps<State>) => { + frame, + formatFactory, + paletteService, +}: VisualizationDimensionEditorProps<State> & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; +}) => { const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; const disabled = !!layer.splitAccessor; - const [color, setColor] = useState(getSeriesColor(layer, accessor)); + const overwriteColor = getSeriesColor(layer, accessor); + const currentColor = useMemo(() => { + if (overwriteColor || !frame.activeData) return overwriteColor; + + const colorAssignments = getColorAssignments( + state.layers, + { tables: frame.activeData }, + formatFactory + ); + const mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + layer, + [accessor], + paletteService + ); + return mappedAccessors[0].color; + }, [overwriteColor, frame, paletteService, state.layers, accessor, formatFactory, layer]); + + const [color, setColor] = useState(currentColor); const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { setColor(text); @@ -596,9 +629,9 @@ const ColorPicker = ({ <EuiColorPicker data-test-subj="indexPattern-dimension-colorPicker" compressed - isClearable + isClearable={Boolean(overwriteColor)} onChange={handleColor} - color={disabled ? '' : color} + color={disabled ? '' : color || currentColor} disabled={disabled} placeholder={i18n.translate('xpack.lens.xyChart.seriesColor.auto', { defaultMessage: 'Auto', From 59a405dc80e63abf118ab1a50a2a8bae2f5e4e14 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin <aleh.zasypkin@gmail.com> Date: Wed, 2 Dec 2020 11:32:22 +0100 Subject: [PATCH 048/107] Make all providers to preserve original URL when session expires. (#84229) --- x-pack/plugins/security/common/constants.ts | 3 + .../common/model/authenticated_user.ts | 3 +- .../model/authentication_provider.test.ts | 21 ++++ .../common/model/authentication_provider.ts | 21 ++++ x-pack/plugins/security/common/model/index.ts | 1 + x-pack/plugins/security/common/parse_next.ts | 12 ++- x-pack/plugins/security/common/types.ts | 8 +- .../logged_out/logged_out_page.test.tsx | 39 +++++++ .../logged_out/logged_out_page.tsx | 3 +- .../authentication/login/login_page.tsx | 7 +- .../public/session/session_expired.ts | 16 ++- .../server/audit/security_audit_logger.ts | 2 +- .../authentication/authenticator.test.ts | 101 +++++++++++++----- .../server/authentication/authenticator.ts | 71 ++++++++---- .../authentication/providers/anonymous.ts | 2 +- .../authentication/providers/base.mock.ts | 2 +- .../server/authentication/providers/base.ts | 2 +- .../authentication/providers/basic.test.ts | 27 ++--- .../server/authentication/providers/basic.ts | 10 +- .../authentication/providers/kerberos.test.ts | 4 +- .../authentication/providers/kerberos.ts | 2 +- .../authentication/providers/oidc.test.ts | 6 +- .../server/authentication/providers/oidc.ts | 17 +-- .../authentication/providers/pki.test.ts | 4 +- .../server/authentication/providers/pki.ts | 2 +- .../authentication/providers/saml.test.ts | 18 ++-- .../server/authentication/providers/saml.ts | 17 +-- .../authentication/providers/token.test.ts | 28 ++--- .../server/authentication/providers/token.ts | 10 +- x-pack/plugins/security/server/config.ts | 2 +- .../routes/views/access_agreement.test.ts | 2 +- .../security/server/routes/views/login.ts | 11 +- .../server/session_management/session.ts | 2 +- .../session_management/session_index.ts | 2 +- .../tests/anonymous/login.ts | 2 +- .../tests/kerberos/kerberos_login.ts | 2 +- .../login_selector/basic_functionality.ts | 2 +- .../tests/pki/pki_auth.ts | 2 +- .../tests/session_idle/cleanup.ts | 2 +- .../tests/session_lifespan/cleanup.ts | 2 +- 40 files changed, 322 insertions(+), 168 deletions(-) create mode 100644 x-pack/plugins/security/common/model/authentication_provider.test.ts create mode 100644 x-pack/plugins/security/common/model/authentication_provider.ts create mode 100644 x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index 07e6ab6c72cb9..f53b5ca6d56ca 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -19,3 +19,6 @@ export const APPLICATION_PREFIX = 'kibana-'; export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint'; +export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider'; +export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg'; +export const NEXT_URL_QUERY_STRING_PARAMETER = 'next'; diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts index c22c5fc4ef0da..491ceb6845e28 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import type { AuthenticationProvider } from '../types'; -import { User } from './user'; +import type { AuthenticationProvider, User } from '.'; const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; diff --git a/x-pack/plugins/security/common/model/authentication_provider.test.ts b/x-pack/plugins/security/common/model/authentication_provider.test.ts new file mode 100644 index 0000000000000..fc32d3108be08 --- /dev/null +++ b/x-pack/plugins/security/common/model/authentication_provider.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shouldProviderUseLoginForm } from './authentication_provider'; + +describe('#shouldProviderUseLoginForm', () => { + ['basic', 'token'].forEach((providerType) => { + it(`returns "true" for "${providerType}" provider`, () => { + expect(shouldProviderUseLoginForm(providerType)).toEqual(true); + }); + }); + + ['anonymous', 'http', 'kerberos', 'oidc', 'pki', 'saml'].forEach((providerType) => { + it(`returns "false" for "${providerType}" provider`, () => { + expect(shouldProviderUseLoginForm(providerType)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security/common/model/authentication_provider.ts b/x-pack/plugins/security/common/model/authentication_provider.ts new file mode 100644 index 0000000000000..1b34fbc9da29a --- /dev/null +++ b/x-pack/plugins/security/common/model/authentication_provider.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Type and name tuple to identify provider used to authenticate user. + */ +export interface AuthenticationProvider { + type: string; + name: string; +} + +/** + * Checks whether authentication provider with the specified type uses Kibana's native login form. + * @param providerType Type of the authentication provider. + */ +export function shouldProviderUseLoginForm(providerType: string) { + return providerType === 'basic' || providerType === 'token'; +} diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 59d4908c67ffb..ee1dcffd4a794 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -7,6 +7,7 @@ export { ApiKey, ApiKeyToInvalidate } from './api_key'; export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; +export { AuthenticationProvider, shouldProviderUseLoginForm } from './authentication_provider'; export { BuiltinESPrivileges } from './builtin_es_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; export { FeaturesPrivileges } from './features_privileges'; diff --git a/x-pack/plugins/security/common/parse_next.ts b/x-pack/plugins/security/common/parse_next.ts index 7ce0de05ad526..68903baeb05b8 100644 --- a/x-pack/plugins/security/common/parse_next.ts +++ b/x-pack/plugins/security/common/parse_next.ts @@ -5,19 +5,21 @@ */ import { parse } from 'url'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from './constants'; import { isInternalURL } from './is_internal_url'; export function parseNext(href: string, basePath = '') { const { query, hash } = parse(href, true); - if (!query.next) { + + let next = query[NEXT_URL_QUERY_STRING_PARAMETER]; + if (!next) { return `${basePath}/`; } - let next: string; - if (Array.isArray(query.next) && query.next.length > 0) { - next = query.next[0]; + if (Array.isArray(next) && next.length > 0) { + next = next[0]; } else { - next = query.next as string; + next = next as string; } // validate that `next` is not attempting a redirect to somewhere diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts index c668c6ccf71d1..33e2875acefef 100644 --- a/x-pack/plugins/security/common/types.ts +++ b/x-pack/plugins/security/common/types.ts @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * Type and name tuple to identify provider used to authenticate user. - */ -export interface AuthenticationProvider { - type: string; - name: string; -} +import type { AuthenticationProvider } from './model'; export interface SessionInfo { now: number; diff --git a/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx new file mode 100644 index 0000000000000..89d622e086b38 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { LoggedOutPage } from './logged_out_page'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +describe('LoggedOutPage', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { href: 'https://some-host' }, + writable: true, + }); + }); + + it('points to a base path if `next` parameter is not provided', async () => { + const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath; + const wrapper = mountWithIntl(<LoggedOutPage basePath={basePathMock} />); + + expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/'); + }); + + it('properly parses `next` parameter', async () => { + window.location.href = `https://host.com/mock-base-path/security/logged_out?next=${encodeURIComponent( + '/mock-base-path/app/home#/?_g=()' + )}`; + + const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath; + const wrapper = mountWithIntl(<LoggedOutPage basePath={basePathMock} />); + + expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/app/home#/?_g=()'); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx index a708931c3fa95..5498b8ef3644c 100644 --- a/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx +++ b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart, IBasePath } from 'src/core/public'; +import { parseNext } from '../../../common/parse_next'; import { AuthenticationStatePage } from '../components'; interface Props { @@ -25,7 +26,7 @@ export function LoggedOutPage({ basePath }: Props) { /> } > - <EuiButton href={basePath.prepend('/')}> + <EuiButton href={parseNext(window.location.href, basePath.serverBasePath)}> <FormattedMessage id="xpack.security.loggedOut.login" defaultMessage="Log in" /> </EuiButton> </AuthenticationStatePage> diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 0646962684284..35703212762fd 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -15,7 +15,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle } from '@elasti import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public'; -import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants'; +import { + AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, + LOGOUT_REASON_QUERY_STRING_PARAMETER, +} from '../../../common/constants'; import { LoginState } from '../../../common/login_state'; import { LoginForm, DisabledLoginForm } from './components'; @@ -219,7 +222,7 @@ export class LoginPage extends Component<Props, State> { http={this.props.http} notifications={this.props.notifications} selector={selector} - infoMessage={infoMessageMap.get(query.msg?.toString())} + infoMessage={infoMessageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} loginHelp={loginHelp} authProviderHint={query[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]?.toString()} diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 5866526b8851e..52ba37c242d09 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + LOGOUT_PROVIDER_QUERY_STRING_PARAMETER, + LOGOUT_REASON_QUERY_STRING_PARAMETER, + NEXT_URL_QUERY_STRING_PARAMETER, +} from '../../common/constants'; + export interface ISessionExpired { logout(): void; } @@ -11,13 +17,15 @@ export interface ISessionExpired { const getNextParameter = () => { const { location } = window; const next = encodeURIComponent(`${location.pathname}${location.search}${location.hash}`); - return `&next=${next}`; + return `&${NEXT_URL_QUERY_STRING_PARAMETER}=${next}`; }; const getProviderParameter = (tenant: string) => { const key = `${tenant}/session_provider`; const providerName = sessionStorage.getItem(key); - return providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; + return providerName + ? `&${LOGOUT_PROVIDER_QUERY_STRING_PARAMETER}=${encodeURIComponent(providerName)}` + : ''; }; export class SessionExpired { @@ -26,6 +34,8 @@ export class SessionExpired { logout() { const next = getNextParameter(); const provider = getProviderParameter(this.tenant); - window.location.assign(`${this.logoutUrl}?msg=SESSION_EXPIRED${next}${provider}`); + window.location.assign( + `${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=SESSION_EXPIRED${next}${provider}` + ); } } diff --git a/x-pack/plugins/security/server/audit/security_audit_logger.ts b/x-pack/plugins/security/server/audit/security_audit_logger.ts index ee81f5f330f44..c0d431b3c2fa2 100644 --- a/x-pack/plugins/security/server/audit/security_audit_logger.ts +++ b/x-pack/plugins/security/server/audit/security_audit_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuthenticationProvider } from '../../common/types'; +import type { AuthenticationProvider } from '../../common/model'; import { LegacyAuditLogger } from './audit_service'; /** diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 16cb51cbfccf5..ed5d05dbcf619 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -111,31 +111,78 @@ describe('Authenticator', () => { ).toThrowError('Provider name "__http__" is reserved.'); }); - it('properly sets `loggedOut` URL.', () => { - const basicAuthenticationProviderMock = jest.requireMock('./providers/basic') - .BasicAuthenticationProvider; + describe('#options.urls.loggedOut', () => { + it('points to /login if provider requires login form', () => { + const authenticationProviderMock = jest.requireMock(`./providers/basic`) + .BasicAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator(getMockOptions()); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; - basicAuthenticationProviderMock.mockClear(); - new Authenticator(getMockOptions()); - expect(basicAuthenticationProviderMock).toHaveBeenCalledWith( - expect.objectContaining({ - urls: { - loggedOut: '/mock-server-basepath/security/logged_out', - }, - }), - expect.anything() - ); + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + '/mock-server-basepath/login?msg=LOGGED_OUT' + ); - basicAuthenticationProviderMock.mockClear(); - new Authenticator(getMockOptions({ selector: { enabled: true } })); - expect(basicAuthenticationProviderMock).toHaveBeenCalledWith( - expect.objectContaining({ - urls: { - loggedOut: `/mock-server-basepath/login?msg=LOGGED_OUT`, - }, - }), - expect.anything() - ); + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED'); + }); + + it('points to /login if login selector is enabled', () => { + const authenticationProviderMock = jest.requireMock(`./providers/saml`) + .SAMLAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator( + getMockOptions({ + selector: { enabled: true }, + providers: { saml: { saml1: { order: 0, realm: 'realm' } } }, + }) + ); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; + + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + '/mock-server-basepath/login?msg=LOGGED_OUT' + ); + + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED'); + }); + + it('points to /security/logged_out if login selector is NOT enabled', () => { + const authenticationProviderMock = jest.requireMock(`./providers/saml`) + .SAMLAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator( + getMockOptions({ + selector: { enabled: false }, + providers: { saml: { saml1: { order: 0, realm: 'realm' } } }, + }) + ); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; + + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + '/mock-server-basepath/security/logged_out?msg=LOGGED_OUT' + ); + + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe( + '/mock-server-basepath/security/logged_out?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED' + ); + }); }); describe('HTTP authentication provider', () => { @@ -1769,7 +1816,9 @@ describe('Authenticator', () => { }); it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { - const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic1' } }); + const request = httpServerMock.createKibanaRequest({ + query: { provider: 'basic1' }, + }); mockOptions.session.get.mockResolvedValue(null); mockBasicAuthenticationProvider.logout.mockResolvedValue( @@ -1782,7 +1831,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.clear).toHaveBeenCalled(); }); it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => { @@ -1811,7 +1860,7 @@ describe('Authenticator', () => { ); expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.clear).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 718415e485725..f175f47d30351 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -10,10 +10,15 @@ import { ILegacyClusterClient, IBasePath, } from '../../../../../src/core/server'; -import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../common/constants'; +import { + AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, + LOGOUT_PROVIDER_QUERY_STRING_PARAMETER, + LOGOUT_REASON_QUERY_STRING_PARAMETER, + NEXT_URL_QUERY_STRING_PARAMETER, +} from '../../common/constants'; import type { SecurityLicense } from '../../common/licensing'; -import type { AuthenticatedUser } from '../../common/model'; -import type { AuthenticationProvider } from '../../common/types'; +import type { AuthenticatedUser, AuthenticationProvider } from '../../common/model'; +import { shouldProviderUseLoginForm } from '../../common/model'; import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit'; import type { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; @@ -199,11 +204,6 @@ export class Authenticator { client: this.options.clusterClient, logger: this.options.loggers.get('tokens'), }), - urls: { - loggedOut: options.config.authc.selector.enabled - ? `${options.basePath.serverBasePath}/login?msg=LOGGED_OUT` - : `${options.basePath.serverBasePath}/security/logged_out`, - }, }; this.providers = new Map( @@ -218,6 +218,7 @@ export class Authenticator { ...providerCommonOptions, name, logger: options.loggers.get(type, name), + urls: { loggedOut: (request) => this.getLoggedOutURL(request, type) }, }), this.options.config.authc.providers[type]?.[name] ), @@ -232,6 +233,9 @@ export class Authenticator { ...providerCommonOptions, name: '__http__', logger: options.loggers.get(HTTPAuthenticationProvider.type), + urls: { + loggedOut: (request) => this.getLoggedOutURL(request, HTTPAuthenticationProvider.type), + }, }) ); } @@ -338,7 +342,9 @@ export class Authenticator { if (this.shouldRedirectToLoginSelector(request, existingSessionValue)) { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( - `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( + `${ + this.options.basePath.serverBasePath + }/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent( `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}${ suggestedProviderName && !existingSessionValue @@ -385,20 +391,17 @@ export class Authenticator { assertRequest(request); const sessionValue = await this.getSessionValue(request); - if (sessionValue) { + const suggestedProviderName = + sessionValue?.provider.name ?? + request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER); + if (suggestedProviderName) { await this.session.clear(request); - return this.providers - .get(sessionValue.provider.name)! - .logout(request, sessionValue.state ?? null); - } - const queryStringProviderName = (request.query as Record<string, string>)?.provider; - if (queryStringProviderName) { - // provider name is passed in a query param and sourced from the browser's local storage; - // hence, we can't assume that this provider exists, so we have to check it - const provider = this.providers.get(queryStringProviderName); + // Provider name may be passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it. + const provider = this.providers.get(suggestedProviderName); if (provider) { - return provider.logout(request, null); + return provider.logout(request, sessionValue?.state ?? null); } } else { // In case logout is called and we cannot figure out what provider is supposed to handle it, @@ -737,7 +740,7 @@ export class Authenticator { // redirect URL in the `next` parameter. Redirect URL provided in authentication result, if any, // always takes precedence over what is specified in `redirectURL` parameter. if (preAccessRedirectURL) { - preAccessRedirectURL = `${preAccessRedirectURL}?next=${encodeURIComponent( + preAccessRedirectURL = `${preAccessRedirectURL}?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent( authenticationResult.redirectURL || redirectURL || `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` @@ -754,4 +757,30 @@ export class Authenticator { }) : authenticationResult; } + + /** + * Creates a logged out URL for the specified request and provider. + * @param request Request that initiated logout. + * @param providerType Type of the provider that handles logout. + */ + private getLoggedOutURL(request: KibanaRequest, providerType: string) { + // The app that handles logout needs to know the reason of the logout and the URL we may need to + // redirect user to once they log in again (e.g. when session expires). + const searchParams = new URLSearchParams(); + for (const [key, defaultValue] of [ + [NEXT_URL_QUERY_STRING_PARAMETER, null], + [LOGOUT_REASON_QUERY_STRING_PARAMETER, 'LOGGED_OUT'], + ] as Array<[string, string | null]>) { + const value = request.url.searchParams.get(key) || defaultValue; + if (value) { + searchParams.append(key, value); + } + } + + // Query string may contain the path where logout has been called or + // logout reason that login page may need to know. + return this.options.config.authc.selector.enabled || shouldProviderUseLoginForm(providerType) + ? `${this.options.basePath.serverBasePath}/login?${searchParams.toString()}` + : `${this.options.basePath.serverBasePath}/security/logged_out?${searchParams.toString()}`; + } } diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.ts index 6d62d3a909e55..4d1b5f4a74b2f 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.ts @@ -162,7 +162,7 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider return DeauthenticationResult.notHandled(); } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index 1b574e6e44c10..47d961bc8faf8 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -22,7 +22,7 @@ export function mockAuthenticationProviderOptions(options?: { name: string }) { tokens: { refresh: jest.fn(), invalidate: jest.fn() }, name: options?.name ?? 'basic1', urls: { - loggedOut: '/mock-server-basepath/security/logged_out', + loggedOut: jest.fn().mockReturnValue('/mock-server-basepath/security/logged_out'), }, }; } diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index a5a68f2a49315..f1845617c87a4 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -29,7 +29,7 @@ export interface AuthenticationProviderOptions { logger: Logger; tokens: PublicMethodsOf<Tokens>; urls: { - loggedOut: string; + loggedOut: (request: KibanaRequest) => string; }; } diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index 87002ebed5672..4f93e2327da06 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -34,6 +34,8 @@ describe('BasicAuthenticationProvider', () => { let mockOptions: ReturnType<typeof mockAuthenticationProviderOptions>; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions(); + mockOptions.urls.loggedOut.mockReturnValue('/some-logged-out-page'); + provider = new BasicAuthenticationProvider(mockOptions); }); @@ -184,30 +186,13 @@ describe('BasicAuthenticationProvider', () => { ); }); - it('redirects to login view if state is `null`.', async () => { - await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') - ); - }); - - it('always redirects to the login page.', async () => { + it('redirects to the logged out URL.', async () => { await expect(provider.logout(httpServerMock.createKibanaRequest(), {})).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') + DeauthenticationResult.redirectTo('/some-logged-out-page') ); - }); - it('passes query string parameters to the login page.', async () => { - await expect( - provider.logout( - httpServerMock.createKibanaRequest({ - query: { next: '/app/ml', msg: 'SESSION_EXPIRED' }, - }), - {} - ) - ).resolves.toEqual( - DeauthenticationResult.redirectTo( - '/mock-server-basepath/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' - ) + await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/some-logged-out-page') ); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/basic.ts b/x-pack/plugins/security/server/authentication/providers/basic.ts index 28b671346ee7f..6a5ae29dfd832 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.ts @@ -5,6 +5,7 @@ */ import { KibanaRequest } from '../../../../../../src/core/server'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import { canRedirectRequest } from '../can_redirect_request'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -108,7 +109,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Redirecting request to Login page.'); const basePath = this.options.basePath.get(request); return AuthenticationResult.redirectTo( - `${basePath}/login?next=${encodeURIComponent( + `${basePath}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent( `${basePath}${request.url.pathname}${request.url.search}` )}` ); @@ -131,12 +132,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.notHandled(); } - // Query string may contain the path where logout has been called or - // logout reason that login page may need to know. - const queryString = request.url.search || `?msg=LOGGED_OUT`; - return DeauthenticationResult.redirectTo( - `${this.options.basePath.get(request)}/login${queryString}` - ); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index eb4ac8f4dcbed..d368bf90cf360 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -470,7 +470,7 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); @@ -501,7 +501,7 @@ describe('KerberosAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect(provider.logout(request, tokenPair)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index 2e15893d0845f..9bf419c7dacaa 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -124,7 +124,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index 126306c885e53..9988ddd99c395 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -611,10 +611,10 @@ describe('OIDCAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); await expect(provider.logout(request, { nonce: 'x', realm: 'oidc1' })).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); @@ -647,7 +647,7 @@ describe('OIDCAuthenticationProvider', () => { await expect( provider.logout(request, { accessToken, refreshToken, realm: 'oidc1' }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index 250641d1cf174..c46ea37f144e9 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type from 'type-detect'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import type { AuthenticationInfo } from '../../elasticsearch'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; @@ -434,7 +435,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -450,14 +451,18 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ private captureRedirectURL(request: KibanaRequest) { + const searchParams = new URLSearchParams([ + [ + NEXT_URL_QUERY_STRING_PARAMETER, + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`, + ], + ['providerType', this.type], + ['providerName', this.options.name], + ]); return AuthenticationResult.redirectTo( `${ this.options.basePath.serverBasePath - }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` - )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( - this.options.name - )}`, + }/internal/security/capture-url?${searchParams.toString()}`, // Here we indicate that current session, if any, should be invalidated. It is a no-op for the // initial handshake, but is essential when both access and refresh tokens are expired. { state: null } diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index aa85b8a43af4d..763231f7fd0df 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -544,7 +544,7 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); @@ -572,7 +572,7 @@ describe('PKIAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect(provider.logout(request, state)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 974a838127e1d..4bb0ddaa4ee65 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -128,7 +128,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 03c0b7404da39..5cba017e4916b 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -1022,10 +1022,10 @@ describe('SAMLAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); await expect(provider.logout(request, { somethingElse: 'x' } as any)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); @@ -1082,7 +1082,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken, realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { @@ -1103,7 +1103,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken, realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { @@ -1126,7 +1126,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken, realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { @@ -1145,7 +1145,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken: 'x-saml-refresh-token', realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { @@ -1159,7 +1159,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); await expect(provider.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); @@ -1174,7 +1174,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); await expect(provider.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); @@ -1187,7 +1187,7 @@ describe('SAMLAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLResponse: 'xxx yyy' } }); await expect(provider.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index 8f31968e5f639..34639a849d354 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import { KibanaRequest } from '../../../../../../src/core/server'; import { isInternalURL } from '../../../common/is_internal_url'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import type { AuthenticationInfo } from '../../elasticsearch'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -282,7 +283,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -606,14 +607,18 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ private captureRedirectURL(request: KibanaRequest) { + const searchParams = new URLSearchParams([ + [ + NEXT_URL_QUERY_STRING_PARAMETER, + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`, + ], + ['providerType', this.type], + ['providerName', this.options.name], + ]); return AuthenticationResult.redirectTo( `${ this.options.basePath.serverBasePath - }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` - )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( - this.options.name - )}`, + }/internal/security/capture-url?${searchParams.toString()}`, // Here we indicate that current session, if any, should be invalidated. It is a no-op for the // initial handshake, but is essential when both access and refresh tokens are expired. { state: null } diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index e09400e9bb44a..5a600461ef467 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -37,6 +37,8 @@ describe('TokenAuthenticationProvider', () => { let mockOptions: MockAuthenticationProviderOptions; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions({ name: 'token' }); + mockOptions.urls.loggedOut.mockReturnValue('/some-logged-out-page'); + provider = new TokenAuthenticationProvider(mockOptions); }); @@ -347,11 +349,9 @@ describe('TokenAuthenticationProvider', () => { expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); - it('redirects to login view if state is `null`.', async () => { - const request = httpServerMock.createKibanaRequest(); - - await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') + it('redirects to the logged out URL if state is `null`.', async () => { + await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/some-logged-out-page') ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); @@ -372,28 +372,14 @@ describe('TokenAuthenticationProvider', () => { expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); - it('redirects to /login if tokens are invalidated successfully', async () => { + it('redirects to the logged out URL if tokens are invalidated successfully.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect(provider.logout(request, tokenPair)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') - ); - - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - }); - - it('redirects to /login with optional search parameters if tokens are invalidated successfully', async () => { - const request = httpServerMock.createKibanaRequest({ query: { yep: 'nope' } }); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - mockOptions.tokens.invalidate.mockResolvedValue(undefined); - - await expect(provider.logout(request, tokenPair)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?yep=nope') + DeauthenticationResult.redirectTo('/some-logged-out-page') ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 2032db4b0a8f2..67c2d244e75a2 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -6,6 +6,7 @@ import Boom from '@hapi/boom'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { canRedirectRequest } from '../can_redirect_request'; @@ -145,10 +146,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { } } - const queryString = request.url.search || `?msg=LOGGED_OUT`; - return DeauthenticationResult.redirectTo( - `${this.options.basePath.get(request)}/login${queryString}` - ); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -235,6 +233,8 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { const nextURL = encodeURIComponent( `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` ); - return `${this.options.basePath.get(request)}/login?next=${nextURL}`; + return `${this.options.basePath.get( + request + )}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${nextURL}`; } } diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index cf9ab3a78ab71..8d1415e1574d4 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -9,7 +9,7 @@ import type { Duration } from 'moment'; import { schema, Type, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { Logger, config as coreConfig } from '../../../../src/core/server'; -import type { AuthenticationProvider } from '../common/types'; +import type { AuthenticationProvider } from '../common/model'; export type ConfigType = ReturnType<typeof createConfig>; type RawConfigType = TypeOf<typeof ConfigSchema>; diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts index b513230b3ba6f..dfe5faa95ae15 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -14,7 +14,7 @@ import { RequestHandlerContext, } from '../../../../../../src/core/server'; import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; -import { AuthenticationProvider } from '../../../common/types'; +import type { AuthenticationProvider } from '../../../common/model'; import { ConfigType } from '../../config'; import { Session } from '../../session_management'; import { defineAccessAgreementRoutes } from './access_agreement'; diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 93d43d04a86ca..68becb48f596e 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -7,6 +7,11 @@ import { schema } from '@kbn/config-schema'; import { parseNext } from '../../../common/parse_next'; import { LoginState } from '../../../common/login_state'; +import { shouldProviderUseLoginForm } from '../../../common/model'; +import { + LOGOUT_REASON_QUERY_STRING_PARAMETER, + NEXT_URL_QUERY_STRING_PARAMETER, +} from '../../../common/constants'; import { RouteDefinitionParams } from '..'; /** @@ -26,8 +31,8 @@ export function defineLoginRoutes({ validate: { query: schema.object( { - next: schema.maybe(schema.string()), - msg: schema.maybe(schema.string()), + [NEXT_URL_QUERY_STRING_PARAMETER]: schema.maybe(schema.string()), + [LOGOUT_REASON_QUERY_STRING_PARAMETER]: schema.maybe(schema.string()), }, { unknowns: 'allow' } ), @@ -59,7 +64,7 @@ export function defineLoginRoutes({ // Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can // be sure that config is present for every provider in `config.authc.sortedProviders`. const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!; - const usesLoginForm = type === 'basic' || type === 'token'; + const usesLoginForm = shouldProviderUseLoginForm(type); return { type, name, diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 757b1aaeddcbc..4dc83a1abe4af 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -9,7 +9,7 @@ import { randomBytes, createHash } from 'crypto'; import nodeCrypto, { Crypto } from '@elastic/node-crypto'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { KibanaRequest, Logger } from '../../../../../src/core/server'; -import type { AuthenticationProvider } from '../../common/types'; +import type { AuthenticationProvider } from '../../common/model'; import type { ConfigType } from '../config'; import type { SessionIndex, SessionIndexValue } from './session_index'; import type { SessionCookie } from './session_cookie'; diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 96fff41d57503..45b2f4489c195 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -5,7 +5,7 @@ */ import type { ILegacyClusterClient, Logger } from '../../../../../src/core/server'; -import type { AuthenticationProvider } from '../../common/types'; +import type { AuthenticationProvider } from '../../common/model'; import type { ConfigType } from '../config'; export interface SessionIndexOptions { diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts index 559dc7cc78036..eaf999c509741 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/login.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -182,7 +182,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); checkCookieIsCleared(request.cookie(cookies[0])!); - expect(logoutResponse.headers.location).to.be('/security/logged_out'); + expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT'); // Old cookie should be invalidated and not allow API access. const apiResponse = await supertest diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index e63f8cd2ebe32..26edc36563e1c 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -259,7 +259,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); checkCookieIsCleared(request.cookie(cookies[0])!); - expect(logoutResponse.headers.location).to.be('/security/logged_out'); + expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT'); // Token that was stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts index edcc1b5744fe3..a9e83ff7dadce 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts @@ -10,7 +10,7 @@ import { resolve } from 'path'; import url from 'url'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/types'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getStateAndNonce } from '../../fixtures/oidc/oidc_tools'; import { getMutualAuthenticationResponseToken, diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index 93eabe33dc687..0d630dab51cf7 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -307,7 +307,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); checkCookieIsCleared(request.cookie(cookies[0])!); - expect(logoutResponse.headers.location).to.be('/security/logged_out'); + expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT'); }); it('should redirect to home page if session cookie is not provided', async () => { diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index c1e8bb9938986..8251ca3419ac8 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -7,7 +7,7 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/types'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index 59e8c746a6d07..134c9e9b1ad82 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -7,7 +7,7 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/types'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; import { FtrProviderContext } from '../../ftr_provider_context'; From 6a07349bb6f1c46d43df64c0ba6ef3720ed162e5 Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Wed, 2 Dec 2020 11:42:52 +0100 Subject: [PATCH 049/107] Add readme for new palette service (#84512) --- src/plugins/charts/README.md | 4 +++ .../charts/public/services/palettes/README.md | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 src/plugins/charts/public/services/palettes/README.md diff --git a/src/plugins/charts/README.md b/src/plugins/charts/README.md index 31727b7acb7a1..dae7b9695ed60 100644 --- a/src/plugins/charts/README.md +++ b/src/plugins/charts/README.md @@ -27,3 +27,7 @@ Truncated color mappings in `value`/`text` form ## Theme See Theme service [docs](public/services/theme/README.md) + +## Palettes + +See palette service [docs](public/services/palettes/README.md) diff --git a/src/plugins/charts/public/services/palettes/README.md b/src/plugins/charts/public/services/palettes/README.md new file mode 100644 index 0000000000000..3403d422682bd --- /dev/null +++ b/src/plugins/charts/public/services/palettes/README.md @@ -0,0 +1,33 @@ +# Palette Service + +The `palette` service offers a collection of palettes which implement a uniform interface for assigning colors to charts. The service provides methods for switching palettes +easily. It's used by the x-pack plugins `canvas` and `lens`. + +Each palette is allowed to store some state as well which has to be handled by the consumer. + +Palettes are integrated with the expression as well using the `system_palette` and `palette` functions. + +## Using the palette service + +To consume the palette service, use `charts.palettes.getPalettes` to lazily load the async bundle implementing existing palettes. This is recommended to be called in the renderer, not as part of the `setup` or `start` phases of a plugin. + +All palette definitions can be loaded using `paletteService.getAll()`. If the id of the palette is known, it can be fetched using `paleteService.get(id)`. + +One a palette is loaded, there are two ways to request colors - either by fetching a list of colors (`getColors`) or by specifying the chart object to be colored (`getColor`). If possible, using `getColor` is recommended because it allows the palette implementation to apply custom logic to coloring (e.g. lightening up colors or syncing colors) which has to be implemented by the consumer if `getColors` is used). + +### SeriesLayer + +If `getColor` is used, an array of `SeriesLayer` objects has to be passed in. These correspond with the current series in the chart a color has to be determined for. An array is necessary as some charts are constructed hierarchically (e.g. pie charts or treemaps). The array of objects represents the current series with all ancestors up to the corresponding root series. For each layer in the series hierarchy, the number of "sibling" series and the position of the current series has to be specified along with the name of the series. + +## Custom palette + +All palettes are stateless and define their own colors except for the `custom` palette which takes a state of the form +```ts +{ colors: string[]; gradient: boolean } +``` + +This state has to be passed into the `getColors` and `getColor` function to retrieve specific colors. + +## Registering new palettes + +Currently palettes can't be extended dynamically. From 5354008a425808a65f6879b9fcbb18264951b9b3 Mon Sep 17 00:00:00 2001 From: Anton Dosov <anton.dosov@elastic.co> Date: Wed, 2 Dec 2020 11:47:30 +0100 Subject: [PATCH 050/107] [Search] Disable "send to background" when auto-refresh is enabled (#84106) --- x-pack/plugins/data_enhanced/public/plugin.ts | 1 + .../background_session_indicator.stories.tsx | 9 ++++ .../background_session_indicator.test.tsx | 10 ++++ .../background_session_indicator.tsx | 5 +- ...cted_background_session_indicator.test.tsx | 47 +++++++++++++++++-- ...connected_background_session_indicator.tsx | 28 ++++++++++- 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 393a28c289e82..a3b37e47287e5 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -69,6 +69,7 @@ export class DataEnhancedPlugin createConnectedBackgroundSessionIndicator({ sessionService: plugins.data.search.session, application: core.application, + timeFilter: plugins.data.query.timefilter.timefilter, }) ) ), diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx index c7195ea486e2f..4a6a852be755b 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx @@ -26,5 +26,14 @@ storiesOf('components/BackgroundSessionIndicator', module).add('default', () => <div> <BackgroundSessionIndicator state={SessionState.Restored} /> </div> + <div> + <BackgroundSessionIndicator + state={SessionState.Completed} + disabled={true} + disabledReasonText={ + 'Send to background capability is unavailable when auto-refresh is enabled' + } + /> + </div> </> )); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx index f401a460113c7..b7d342300f311 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx @@ -100,3 +100,13 @@ test('Canceled state', async () => { expect(onRefresh).toBeCalled(); }); + +test('Disabled state', async () => { + render( + <Container> + <BackgroundSessionIndicator state={SessionState.Loading} disabled={true} /> + </Container> + ); + + expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx index 674ce392aa2d0..ce77686c4f3c1 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx @@ -30,6 +30,8 @@ export interface BackgroundSessionIndicatorProps { viewBackgroundSessionsLink?: string; onSaveResults?: () => void; onRefresh?: () => void; + disabled?: boolean; + disabledReasonText?: string; } type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; @@ -285,12 +287,13 @@ export const BackgroundSessionIndicator: React.FC<BackgroundSessionIndicatorProp data-test-subj={'backgroundSessionIndicator'} data-state={props.state} button={ - <EuiToolTip content={button.tooltipText}> + <EuiToolTip content={props.disabled ? props.disabledReasonText : button.tooltipText}> <EuiButtonIcon color={button.color} aria-label={button['aria-label']} iconType={button.iconType} onClick={onButtonClick} + disabled={props.disabled} /> </EuiToolTip> } diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx index 4c9fd50dc8c4c..e08773c6a8a76 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx @@ -5,21 +5,36 @@ */ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, screen, act } from '@testing-library/react'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator'; import { BehaviorSubject } from 'rxjs'; -import { ISessionService, SessionState } from '../../../../../../../src/plugins/data/public'; +import { + ISessionService, + RefreshInterval, + SessionState, + TimefilterContract, +} from '../../../../../../../src/plugins/data/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; const coreStart = coreMock.createStart(); -const sessionService = dataPluginMock.createStartContract().search - .session as jest.Mocked<ISessionService>; +const dataStart = dataPluginMock.createStartContract(); +const sessionService = dataStart.search.session as jest.Mocked<ISessionService>; + +const refreshInterval$ = new BehaviorSubject<RefreshInterval>({ value: 0, pause: true }); +const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked<TimefilterContract>; +timeFilter.getRefreshIntervalUpdate$.mockImplementation(() => refreshInterval$); +timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); + +beforeEach(() => { + refreshInterval$.next({ value: 0, pause: true }); +}); test("shouldn't show indicator in case no active search session", async () => { const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService, application: coreStart.application, + timeFilter, }); const { getByTestId, container } = render(<BackgroundSessionIndicator />); @@ -35,8 +50,32 @@ test('should show indicator in case there is an active search session', async () const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, + timeFilter, }); const { getByTestId } = render(<BackgroundSessionIndicator />); await waitFor(() => getByTestId('backgroundSessionIndicator')); }); + +test('should be disabled during auto-refresh', async () => { + const state$ = new BehaviorSubject(SessionState.Loading); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); + + render(<BackgroundSessionIndicator />); + + await waitFor(() => screen.getByTestId('backgroundSessionIndicator')); + + expect( + screen.getByTestId('backgroundSessionIndicator').querySelector('button') + ).not.toBeDisabled(); + + act(() => { + refreshInterval$.next({ value: 0, pause: false }); + }); + + expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx index 1cc2fffcea8c5..b80295d87d202 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx @@ -5,24 +5,46 @@ */ import React from 'react'; -import { debounceTime } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import useObservable from 'react-use/lib/useObservable'; +import { i18n } from '@kbn/i18n'; import { BackgroundSessionIndicator } from '../background_session_indicator'; -import { ISessionService } from '../../../../../../../src/plugins/data/public/'; +import { ISessionService, TimefilterContract } from '../../../../../../../src/plugins/data/public/'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; import { ApplicationStart } from '../../../../../../../src/core/public'; export interface BackgroundSessionIndicatorDeps { sessionService: ISessionService; + timeFilter: TimefilterContract; application: ApplicationStart; } export const createConnectedBackgroundSessionIndicator = ({ sessionService, application, + timeFilter, }: BackgroundSessionIndicatorDeps): React.FC => { + const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; + const isAutoRefreshEnabled$ = timeFilter + .getRefreshIntervalUpdate$() + .pipe(map(isAutoRefreshEnabled), distinctUntilChanged()); + return () => { const state = useObservable(sessionService.state$.pipe(debounceTime(500))); + const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); + let disabled = false; + let disabledReasonText: string = ''; + + if (autoRefreshEnabled) { + disabled = true; + disabledReasonText = i18n.translate( + 'xpack.data.backgroundSessionIndicator.disabledDueToAutoRefreshMessage', + { + defaultMessage: 'Send to background is not available when auto refresh is enabled.', + } + ); + } + if (!state) return null; return ( <RedirectAppLinks application={application}> @@ -40,6 +62,8 @@ export const createConnectedBackgroundSessionIndicator = ({ onCancel={() => { sessionService.cancel(); }} + disabled={disabled} + disabledReasonText={disabledReasonText} /> </RedirectAppLinks> ); From 126c99a175795645ade53ebd013c0fb6817f4b33 Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala <bohdan.tsymbala@elastic.co> Date: Wed, 2 Dec 2020 12:48:11 +0100 Subject: [PATCH 051/107] Added word break styles to the texts in the item details card. (#84654) * Added word break styles to the texts in the item details card. * Updated snapshots. --- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/item_details_card/index.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 300 +++++++++--------- .../__snapshots__/index.test.tsx.snap | 10 +- 4 files changed, 159 insertions(+), 157 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index d95e0300fe140..c7841f6d6bbcc 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -182,7 +182,9 @@ exports[`item_details_card ItemDetailsPropertySummary should render correctly 1` <Styled(EuiDescriptionListTitle)> name 1 </Styled(EuiDescriptionListTitle)> - <Styled(EuiDescriptionListDescription)> + <Styled(EuiDescriptionListDescription) + className="eui-textBreakWord" + > value 1 </Styled(EuiDescriptionListDescription)> </Fragment> diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 829d8db5a5a0f..8f32224f860e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -72,7 +72,7 @@ export const ItemDetailsPropertySummary = memo<ItemDetailsPropertySummaryProps>( ({ name, value }) => ( <> <DescriptionListTitle>{name}</DescriptionListTitle> - <DescriptionListDescription>{value}</DescriptionListDescription> + <DescriptionListDescription className="eui-textBreakWord">{value}</DescriptionListDescription> </> ) ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 71b103949a80a..eb3a8f2132e7f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -138,7 +138,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -154,7 +154,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -170,7 +170,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -180,7 +180,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -196,7 +196,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -390,7 +390,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -406,7 +406,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -422,7 +422,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -432,7 +432,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -448,7 +448,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -642,7 +642,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -658,7 +658,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -674,7 +674,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -684,7 +684,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -700,7 +700,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -894,7 +894,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -910,7 +910,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -926,7 +926,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -936,7 +936,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -952,7 +952,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1146,7 +1146,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1162,7 +1162,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1178,7 +1178,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1188,7 +1188,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1204,7 +1204,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1398,7 +1398,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1414,7 +1414,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1430,7 +1430,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1440,7 +1440,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1456,7 +1456,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1650,7 +1650,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1666,7 +1666,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1682,7 +1682,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1692,7 +1692,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1708,7 +1708,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1902,7 +1902,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1918,7 +1918,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1934,7 +1934,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1944,7 +1944,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1960,7 +1960,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2154,7 +2154,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2170,7 +2170,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2186,7 +2186,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -2196,7 +2196,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2212,7 +2212,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2406,7 +2406,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2422,7 +2422,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2438,7 +2438,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -2448,7 +2448,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2464,7 +2464,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2948,7 +2948,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2964,7 +2964,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2980,7 +2980,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -2990,7 +2990,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3006,7 +3006,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3200,7 +3200,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3216,7 +3216,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3232,7 +3232,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3242,7 +3242,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3258,7 +3258,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3452,7 +3452,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3468,7 +3468,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3484,7 +3484,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3494,7 +3494,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3510,7 +3510,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3704,7 +3704,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3720,7 +3720,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3736,7 +3736,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3746,7 +3746,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3762,7 +3762,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3956,7 +3956,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3972,7 +3972,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3988,7 +3988,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3998,7 +3998,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4014,7 +4014,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4208,7 +4208,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4224,7 +4224,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4240,7 +4240,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -4250,7 +4250,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4266,7 +4266,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4460,7 +4460,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4476,7 +4476,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4492,7 +4492,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -4502,7 +4502,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4518,7 +4518,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4712,7 +4712,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4728,7 +4728,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4744,7 +4744,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -4754,7 +4754,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4770,7 +4770,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4964,7 +4964,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4980,7 +4980,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4996,7 +4996,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -5006,7 +5006,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5022,7 +5022,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5216,7 +5216,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5232,7 +5232,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5248,7 +5248,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -5258,7 +5258,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5274,7 +5274,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5716,7 +5716,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5732,7 +5732,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5748,7 +5748,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -5758,7 +5758,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5774,7 +5774,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5968,7 +5968,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5984,7 +5984,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6000,7 +6000,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6010,7 +6010,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6026,7 +6026,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6220,7 +6220,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6236,7 +6236,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6252,7 +6252,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6262,7 +6262,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6278,7 +6278,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6472,7 +6472,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6488,7 +6488,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6504,7 +6504,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6514,7 +6514,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6530,7 +6530,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6724,7 +6724,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6740,7 +6740,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6756,7 +6756,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6766,7 +6766,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6782,7 +6782,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6976,7 +6976,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6992,7 +6992,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7008,7 +7008,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7018,7 +7018,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7034,7 +7034,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7228,7 +7228,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7244,7 +7244,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7260,7 +7260,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7270,7 +7270,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7286,7 +7286,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7480,7 +7480,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7496,7 +7496,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7512,7 +7512,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7522,7 +7522,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7538,7 +7538,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7732,7 +7732,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7748,7 +7748,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7764,7 +7764,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7774,7 +7774,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7790,7 +7790,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7984,7 +7984,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -8000,7 +8000,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -8016,7 +8016,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -8026,7 +8026,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -8042,7 +8042,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 4a8f984b59a01..8f70c61ba4afc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -793,7 +793,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -809,7 +809,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -825,7 +825,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -835,7 +835,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -851,7 +851,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" From 452f841491d6ea2ff9ebf1b712a582aede6d1ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= <sorenlouv@gmail.com> Date: Wed, 2 Dec 2020 13:10:27 +0100 Subject: [PATCH 052/107] [APM] Refactor hooks and context (#84615) * Refactor hooks and context * Fix tsc issues * address feedback * Fix lint * type-only import --- .../anomaly_detection_setup_link.test.tsx | 2 +- .../anomaly_detection_setup_link.tsx | 10 ++--- .../public/application/action_menu/index.tsx | 2 +- .../public/application/application.test.tsx | 2 +- .../plugins/apm/public/application/csmApp.tsx | 4 +- .../plugins/apm/public/application/index.tsx | 6 +-- .../ErrorCountAlertTrigger/index.stories.tsx | 4 +- .../alerting/ErrorCountAlertTrigger/index.tsx | 10 +++-- .../index.stories.tsx | 6 +-- .../TransactionDurationAlertTrigger/index.tsx | 14 ++++--- .../index.tsx | 14 ++++--- .../index.tsx | 14 ++++--- .../app/Correlations/ErrorCorrelations.tsx | 4 +- .../app/Correlations/LatencyCorrelations.tsx | 4 +- .../Correlations/SignificantTermsTable.tsx | 2 +- .../components/app/Correlations/index.tsx | 4 +- .../ErrorGroupDetails/DetailView/index.tsx | 2 +- .../ErrorGroupDetails/Distribution/index.tsx | 2 +- .../app/ErrorGroupDetails/index.tsx | 27 ++++--------- .../List/__test__/List.test.tsx | 4 +- .../app/ErrorGroupOverview/List/index.tsx | 2 +- .../app/ErrorGroupOverview/index.tsx | 28 ++++--------- .../public/components/app/Home/Home.test.tsx | 2 +- .../app/Main/route_config/index.tsx | 2 +- .../route_handlers/agent_configuration.tsx | 2 +- .../RumDashboard/Charts/PageViewsChart.tsx | 2 +- .../app/RumDashboard/ClientMetrics/index.tsx | 2 +- .../ImpactfulMetrics/JSErrors.tsx | 4 +- .../PageLoadDistribution/index.tsx | 4 +- .../PageLoadDistribution/use_breakdowns.ts | 4 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 4 +- .../app/RumDashboard/Panels/MainFilters.tsx | 4 +- .../app/RumDashboard/RumDashboard.tsx | 2 +- .../URLFilter/ServiceNameFilter/index.tsx | 2 +- .../__tests__/SelectableUrlList.test.tsx | 2 +- .../URLFilter/URLSearch/index.tsx | 4 +- .../app/RumDashboard/URLFilter/index.tsx | 4 +- .../RumDashboard/UXMetrics/KeyUXMetrics.tsx | 2 +- .../UXMetrics/__tests__/KeyUXMetrics.test.tsx | 2 +- .../app/RumDashboard/UXMetrics/index.tsx | 4 +- .../app/RumDashboard/UserPercentile/index.tsx | 2 +- .../RumDashboard/VisitorBreakdown/index.tsx | 4 +- .../VisitorBreakdownMap/EmbeddedMap.tsx | 2 +- .../VisitorBreakdownMap/useLayerList.ts | 2 +- .../VisitorBreakdownMap/useMapFilters.ts | 2 +- .../app/RumDashboard/hooks/useUxQuery.ts | 2 +- .../app/RumDashboard/utils/test_helper.tsx | 2 +- .../app/ServiceMap/Controls.test.tsx | 2 +- .../components/app/ServiceMap/Controls.tsx | 6 +-- .../components/app/ServiceMap/Cytoscape.tsx | 2 +- .../components/app/ServiceMap/EmptyBanner.tsx | 2 +- .../ServiceMap/Popover/AnomalyDetection.tsx | 2 +- .../app/ServiceMap/Popover/Buttons.test.tsx | 2 +- .../app/ServiceMap/Popover/Buttons.tsx | 4 +- .../ServiceMap/Popover/Popover.stories.tsx | 4 +- .../Popover/ServiceStatsFetcher.tsx | 4 +- .../app/ServiceMap/Popover/index.tsx | 2 +- .../app/ServiceMap/cytoscape_options.ts | 2 +- .../app/ServiceMap/empty_banner.test.tsx | 2 +- .../components/app/ServiceMap/index.test.tsx | 6 +-- .../components/app/ServiceMap/index.tsx | 10 ++--- .../app/ServiceNodeOverview/index.tsx | 4 +- .../ServicePage/ServicePage.tsx | 2 +- .../SettingsPage/SettingsPage.tsx | 4 +- .../index.stories.tsx | 4 +- .../AgentConfigurationCreateEdit/index.tsx | 2 +- .../List/ConfirmDeleteModal.tsx | 2 +- .../AgentConfigurations/List/index.tsx | 6 +-- .../Settings/AgentConfigurations/index.tsx | 4 +- .../app/Settings/ApmIndices/index.test.tsx | 4 +- .../app/Settings/ApmIndices/index.tsx | 4 +- .../DeleteButton.tsx | 2 +- .../CreateEditCustomLinkFlyout/index.tsx | 2 +- .../CustomizeUI/CustomLink/index.test.tsx | 6 +-- .../Settings/CustomizeUI/CustomLink/index.tsx | 6 +-- .../components/app/Settings/Settings.test.tsx | 2 +- .../anomaly_detection/add_environments.tsx | 4 +- .../app/Settings/anomaly_detection/index.tsx | 8 ++-- .../Settings/anomaly_detection/jobs_list.tsx | 2 +- .../anomaly_detection/legacy_jobs_callout.tsx | 2 +- .../public/components/app/Settings/index.tsx | 2 +- .../public/components/app/TraceLink/index.tsx | 4 +- .../app/TraceLink/trace_link.test.tsx | 8 ++-- .../components/app/TraceOverview/index.tsx | 4 +- .../TransactionDetails/Distribution/index.tsx | 4 +- .../WaterfallWithSummmary/TransactionTabs.tsx | 2 +- .../WaterfallContainer.stories.tsx | 2 +- .../WaterfallContainer/index.tsx | 2 +- .../waterfallContainer.stories.data.ts | 2 +- .../WaterfallWithSummmary/index.tsx | 2 +- .../app/TransactionDetails/index.tsx | 32 ++++++++------- .../use_waterfall_fetcher.ts} | 9 +++-- .../service_details/service_detail_tabs.tsx | 6 +-- .../ServiceList/HealthBadge.tsx | 2 +- .../ServiceList/service_list.test.tsx | 2 +- .../app/service_inventory/index.tsx | 35 +++++++++------- .../no_services_message.test.tsx | 4 +- .../service_inventory/no_services_message.tsx | 2 +- .../service_inventory.test.tsx | 28 +++++-------- .../use_anomaly_detection_jobs_fetcher.ts} | 13 +++--- .../components/app/service_metrics/index.tsx | 10 +++-- .../app/service_node_metrics/index.test.tsx | 2 +- .../app/service_node_metrics/index.tsx | 18 ++++----- .../components/app/service_overview/index.tsx | 2 +- .../service_overview.test.tsx | 20 +++++----- .../service_overview_errors_table/index.tsx | 4 +- .../service_overview_throughput_chart.tsx | 10 ++--- .../index.tsx | 4 +- .../TransactionList.stories.tsx | 2 +- .../app/transaction_overview/index.tsx | 26 ++++++------ .../transaction_overview.test.tsx | 28 +++++++------ .../use_transaction_list.ts} | 17 ++++---- .../user_experience_callout.tsx | 2 +- .../shared/ApmHeader/apm_header.stories.tsx | 4 +- .../components/shared/ApmHeader/index.tsx | 2 +- .../shared/DatePicker/date_picker.test.tsx | 6 +-- .../components/shared/DatePicker/index.tsx | 4 +- .../shared/EnvironmentFilter/index.tsx | 6 +-- .../shared/KueryBar/get_bool_filter.ts | 2 +- .../components/shared/KueryBar/index.tsx | 8 ++-- .../LicensePrompt/LicensePrompt.stories.tsx | 2 +- .../Links/DiscoverLinks/DiscoverLink.tsx | 2 +- .../shared/Links/ElasticDocsLink.tsx | 2 +- .../components/shared/Links/InfraLink.tsx | 2 +- .../components/shared/Links/KibanaLink.tsx | 2 +- .../Links/MachineLearningLinks/MLLink.tsx | 4 +- .../useTimeSeriesExplorerHref.ts | 4 +- .../shared/Links/SetupInstructionsLink.tsx | 2 +- .../components/shared/Links/apm/APMLink.tsx | 4 +- .../shared/Links/apm/ErrorOverviewLink.tsx | 2 +- .../shared/Links/apm/MetricOverviewLink.tsx | 2 +- .../apm/ServiceNodeMetricOverviewLink.tsx | 2 +- .../Links/apm/ServiceNodeOverviewLink.tsx | 2 +- .../shared/Links/apm/TraceOverviewLink.tsx | 2 +- .../Links/apm/TransactionDetailLink.tsx | 2 +- .../Links/apm/TransactionOverviewLink.tsx | 2 +- .../Links/apm/service_inventory_link.tsx | 2 +- .../TransactionTypeFilter/index.tsx | 2 +- .../components/shared/ManagedTable/index.tsx | 2 +- .../__test__/ErrorMetadata.test.tsx | 2 +- .../__test__/SpanMetadata.test.tsx | 2 +- .../__test__/TransactionMetadata.test.tsx | 2 +- .../__test__/MetadataTable.test.tsx | 2 +- .../components/shared/MetadataTable/index.tsx | 2 +- .../Summary/ErrorCountSummaryItemBadge.tsx | 2 +- .../CustomLinkToolbar.test.tsx | 2 +- .../CustomLinkMenuSection/index.test.tsx | 4 +- .../CustomLinkMenuSection/index.tsx | 2 +- .../TransactionActionMenu.tsx | 8 ++-- .../__test__/TransactionActionMenu.test.tsx | 6 +-- .../shared/TransactionActionMenu/sections.ts | 2 +- .../components/shared/charts/Legend/index.tsx | 2 +- .../charts/Timeline/Marker/AgentMarker.tsx | 2 +- .../Timeline/Marker/ErrorMarker.test.tsx | 2 +- .../charts/Timeline/Marker/ErrorMarker.tsx | 4 +- .../shared/charts/Timeline/TimelineAxis.tsx | 2 +- .../shared/charts/Timeline/VerticalLines.tsx | 2 +- .../shared/charts/chart_container.test.tsx | 2 +- .../shared/charts/chart_container.tsx | 2 +- .../shared/charts/metrics_chart/index.tsx | 2 +- .../spark_plot_with_value_label/index.tsx | 2 +- .../shared/charts/timeseries_chart.tsx | 14 +++---- .../transaction_breakdown_chart/index.tsx | 2 +- .../transaction_breakdown_chart_contents.tsx | 14 +++---- .../use_transaction_breakdown.ts | 8 ++-- .../charts/transaction_charts/index.tsx | 10 ++--- .../charts/transaction_charts/ml_header.tsx | 2 +- .../transaction_error_rate_chart/index.tsx | 6 +-- .../shared/table_fetch_wrapper/index.tsx | 2 +- .../{ => annotations}/annotations_context.tsx | 8 ++-- .../annotations/use_annotations_context.ts} | 4 +- .../apm_plugin_context.tsx} | 2 +- .../mock_apm_plugin_context.tsx} | 2 +- .../apm_plugin/use_apm_plugin_context.ts} | 2 +- .../apm_service_context.test.tsx | 0 .../{ => apm_service}/apm_service_context.tsx | 16 ++++---- .../apm_service/use_apm_service_context.ts} | 4 +- .../use_service_agent_name_fetcher.ts} | 6 +-- ...use_service_transaction_types_fetcher.tsx} | 6 +-- .../chart_pointer_event_context.tsx | 0 .../use_chart_pointer_event_context.tsx} | 4 +- .../Invalid_license_notification.tsx} | 0 .../index.tsx => license/license_context.tsx} | 4 +- .../license/use_license_context.ts} | 4 +- .../constants.ts | 0 .../helpers.ts | 0 .../mock_url_params_context_provider.tsx} | 2 +- .../resolve_url_params.ts} | 0 .../types.ts | 0 .../url_params_context.test.tsx | 2 +- .../url_params_context.tsx} | 2 +- .../url_params_context/use_url_params.tsx} | 2 +- x-pack/plugins/apm/public/hooks/useCallApi.ts | 2 +- .../plugins/apm/public/hooks/useKibanaUrl.ts | 2 +- .../apm/public/hooks/useLocalUIFilters.ts | 6 +-- .../apm/public/hooks/use_breadcrumbs.test.tsx | 4 +- .../apm/public/hooks/use_breadcrumbs.ts | 2 +- ...attern.ts => use_dynamic_index_pattern.ts} | 4 +- ...ments.tsx => use_environments_fetcher.tsx} | 4 +- .../use_error_group_distribution_fetcher.tsx | 40 +++++++++++++++++++ .../hooks/use_fetcher.integration.test.tsx | 4 +- ...eFetcher.test.tsx => use_fetcher.test.tsx} | 6 +-- .../hooks/{useFetcher.tsx => use_fetcher.tsx} | 2 +- ...s => use_service_metric_charts_fetcher.ts} | 19 ++++----- .../hooks/{useTheme.tsx => use_theme.tsx} | 0 ...s.ts => use_transaction_charts_fetcher.ts} | 12 +++--- ...> use_transaction_distribution_fetcher.ts} | 16 ++++---- .../apm/public/selectors/chart_selectors.ts | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 4 +- 209 files changed, 544 insertions(+), 512 deletions(-) rename x-pack/plugins/apm/public/{hooks/useWaterfall.ts => components/app/TransactionDetails/use_waterfall_fetcher.ts} (75%) rename x-pack/plugins/apm/public/{hooks/useAnomalyDetectionJobs.ts => components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts} (50%) rename x-pack/plugins/apm/public/{hooks/useTransactionList.ts => components/app/transaction_overview/use_transaction_list.ts} (75%) rename x-pack/plugins/apm/public/{hooks => components/shared/charts/transaction_breakdown_chart}/use_transaction_breakdown.ts (80%) rename x-pack/plugins/apm/public/context/{ => annotations}/annotations_context.tsx (83%) rename x-pack/plugins/apm/public/{hooks/use_annotations.ts => context/annotations/use_annotations_context.ts} (80%) rename x-pack/plugins/apm/public/context/{ApmPluginContext/index.tsx => apm_plugin/apm_plugin_context.tsx} (94%) rename x-pack/plugins/apm/public/context/{ApmPluginContext/MockApmPluginContext.tsx => apm_plugin/mock_apm_plugin_context.tsx} (97%) rename x-pack/plugins/apm/public/{hooks/useApmPluginContext.ts => context/apm_plugin/use_apm_plugin_context.ts} (84%) rename x-pack/plugins/apm/public/context/{ => apm_service}/apm_service_context.test.tsx (100%) rename x-pack/plugins/apm/public/context/{ => apm_service}/apm_service_context.tsx (75%) rename x-pack/plugins/apm/public/{hooks/use_apm_service.ts => context/apm_service/use_apm_service_context.ts} (75%) rename x-pack/plugins/apm/public/{hooks/use_service_agent_name.ts => context/apm_service/use_service_agent_name_fetcher.ts} (83%) rename x-pack/plugins/apm/public/{hooks/use_service_transaction_types.tsx => context/apm_service/use_service_transaction_types_fetcher.tsx} (83%) rename x-pack/plugins/apm/public/context/{ => chart_pointer_event}/chart_pointer_event_context.tsx (100%) rename x-pack/plugins/apm/public/{hooks/use_chart_pointer_event.tsx => context/chart_pointer_event/use_chart_pointer_event_context.tsx} (78%) rename x-pack/plugins/apm/public/context/{LicenseContext/InvalidLicenseNotification.tsx => license/Invalid_license_notification.tsx} (100%) rename x-pack/plugins/apm/public/context/{LicenseContext/index.tsx => license/license_context.tsx} (87%) rename x-pack/plugins/apm/public/{hooks/useLicense.ts => context/license/use_license_context.ts} (77%) rename x-pack/plugins/apm/public/context/{UrlParamsContext => url_params_context}/constants.ts (100%) rename x-pack/plugins/apm/public/context/{UrlParamsContext => url_params_context}/helpers.ts (100%) rename x-pack/plugins/apm/public/context/{UrlParamsContext/MockUrlParamsContextProvider.tsx => url_params_context/mock_url_params_context_provider.tsx} (93%) rename x-pack/plugins/apm/public/context/{UrlParamsContext/resolveUrlParams.ts => url_params_context/resolve_url_params.ts} (100%) rename x-pack/plugins/apm/public/context/{UrlParamsContext => url_params_context}/types.ts (100%) rename x-pack/plugins/apm/public/context/{UrlParamsContext => url_params_context}/url_params_context.test.tsx (98%) rename x-pack/plugins/apm/public/context/{UrlParamsContext/index.tsx => url_params_context/url_params_context.tsx} (98%) rename x-pack/plugins/apm/public/{hooks/useUrlParams.tsx => context/url_params_context/use_url_params.tsx} (84%) rename x-pack/plugins/apm/public/hooks/{useDynamicIndexPattern.ts => use_dynamic_index_pattern.ts} (89%) rename x-pack/plugins/apm/public/hooks/{useEnvironments.tsx => use_environments_fetcher.tsx} (94%) create mode 100644 x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx rename x-pack/plugins/apm/public/hooks/{useFetcher.test.tsx => use_fetcher.test.tsx} (96%) rename x-pack/plugins/apm/public/hooks/{useFetcher.tsx => use_fetcher.tsx} (98%) rename x-pack/plugins/apm/public/hooks/{useServiceMetricCharts.ts => use_service_metric_charts_fetcher.ts} (75%) rename x-pack/plugins/apm/public/hooks/{useTheme.tsx => use_theme.tsx} (100%) rename x-pack/plugins/apm/public/hooks/{useTransactionCharts.ts => use_transaction_charts_fetcher.ts} (82%) rename x-pack/plugins/apm/public/hooks/{useTransactionDistribution.ts => use_transaction_distribution_fetcher.ts} (89%) diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx index b90f606d276eb..399a24a8bc165 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { MissingJobsAlert } from './anomaly_detection_setup_link'; -import * as hooks from '../../hooks/useFetcher'; +import * as hooks from '../../hooks/use_fetcher'; async function renderTooltipAnchor({ jobs, diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index a06d6f357c524..8a1d73c818944 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -16,10 +16,10 @@ import { getEnvironmentLabel, } from '../../../common/environment_filter_values'; import { getAPMHref } from '../../components/shared/Links/apm/APMLink'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; -import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher'; -import { useLicense } from '../../hooks/useLicense'; -import { useUrlParams } from '../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; +import { useLicenseContext } from '../../context/license/use_license_context'; +import { useUrlParams } from '../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { units } from '../../style/variables'; @@ -32,7 +32,7 @@ export function AnomalyDetectionSetupLink() { const environment = uiFilters.environment; const { core } = useApmPluginContext(); const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; - const license = useLicense(); + const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const { basePath } = core.http; diff --git a/x-pack/plugins/apm/public/application/action_menu/index.tsx b/x-pack/plugins/apm/public/application/action_menu/index.tsx index 1713ef61fac1e..438eb2bca7f24 100644 --- a/x-pack/plugins/apm/public/application/action_menu/index.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { getAlertingCapabilities } from '../../components/alerting/get_alert_capabilities'; import { getAPMHref } from '../../components/shared/Links/apm/APMLink'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 75b7835c13151..c5091b1b554cc 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -8,7 +8,7 @@ import { act } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public'; -import { mockApmPluginContextValue } from '../context/ApmPluginContext/MockApmPluginContext'; +import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { ApmPluginSetupDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { renderApp } from './'; diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 7fcbe7c518cd0..4d16643a83fe9 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -20,8 +20,8 @@ import { APMRouteDefinition } from '../application/routes'; import { renderAsRedirectTo } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; import { RumHome, UX_LABEL } from '../components/app/RumDashboard/RumHome'; -import { ApmPluginContext } from '../context/ApmPluginContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; +import { ApmPluginContext } from '../context/apm_plugin/apm_plugin_context'; +import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ConfigSchema } from '../index'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 79c29867cb8e3..9c4413765a500 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -25,9 +25,9 @@ import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPat import { ApmPluginContext, ApmPluginContextValue, -} from '../context/ApmPluginContext'; -import { LicenseProvider } from '../context/LicenseContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; +} from '../context/apm_plugin/apm_plugin_context'; +import { LicenseProvider } from '../context/license/license_context'; +import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ApmPluginSetupDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx index 1a565ab8708bc..f4f2be0a6e889 100644 --- a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ErrorCountAlertTrigger } from '.'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; export default { title: 'app/ErrorCountAlertTrigger', diff --git a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx index a465b90e7bf05..efa792ff44273 100644 --- a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx @@ -10,8 +10,8 @@ import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; @@ -34,7 +34,11 @@ export function ErrorCountAlertTrigger(props: Props) { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); const defaults = { threshold: 25, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx index d20aae29fb8ce..8b2d4e235ac25 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx @@ -8,12 +8,12 @@ import { cloneDeep, merge } from 'lodash'; import React, { ComponentType } from 'react'; import { MemoryRouter, Route } from 'react-router-dom'; import { TransactionDurationAlertTrigger } from '.'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; export default { title: 'app/TransactionDurationAlertTrigger', diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx index b7220de8079c9..3566850aa24c4 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx @@ -10,8 +10,8 @@ import { map } from 'lodash'; import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; @@ -21,7 +21,7 @@ import { TransactionTypeField, IsAboveField, } from '../fields'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface AlertParams { windowSize: number; @@ -63,10 +63,14 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmService(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); if (!transactionTypes.length || !serviceName) { return null; diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index e13ed6c1bcd6f..ff5939c601375 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { @@ -23,7 +23,7 @@ import { ServiceField, TransactionTypeField, } from '../fields'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface Params { windowSize: number; @@ -47,10 +47,14 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmService(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); if (serviceName && !transactionTypes.length) { return null; diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx index 464409ed332e8..f723febde389d 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -7,8 +7,8 @@ import { useParams } from 'react-router-dom'; import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; @@ -18,7 +18,7 @@ import { EnvironmentField, IsAboveField, } from '../fields'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface AlertParams { windowSize: number; @@ -38,10 +38,14 @@ interface Props { export function TransactionErrorRateAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmService(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); if (serviceName && !transactionTypes.length) { return null; diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx index 3ad71b52b6037..07ab89afd4108 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx @@ -17,8 +17,8 @@ import { import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType, callApmApi, diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx index 4364731501b89..30659cf3f9319 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx @@ -19,8 +19,8 @@ import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { getDurationFormatter } from '../../../../common/utils/formatters'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType, callApmApi, diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx index b74517902f89b..350f64367b766 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx @@ -10,7 +10,7 @@ import { useHistory } from 'react-router-dom'; import { EuiBasicTable } from '@elastic/eui'; import { asPercent, asInteger } from '../../../../common/utils/formatters'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { createHref } from '../../shared/Links/url_helpers'; type CorrelationsApiResponse = diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx index b0f6b83485e39..16a21e28fc08d 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx @@ -19,10 +19,10 @@ import { } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { enableCorrelations } from '../../../../common/ui_settings_keys'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { LatencyCorrelations } from './LatencyCorrelations'; import { ErrorCorrelations } from './ErrorCorrelations'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { createHref } from '../../shared/Links/url_helpers'; export function Correlations() { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 643064b2f3176..c0ce2ed388a12 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -22,7 +22,7 @@ import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; import { px, unit, units } from '../../../../style/variables'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 159f111bee04c..ab99c6ffa8da0 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -20,7 +20,7 @@ import d3 from 'd3'; import React from 'react'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index dc97642dec357..95ebd5d4036de 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -21,14 +21,15 @@ import { RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; import { SearchBar } from '../../shared/search_bar'; import { DetailView } from './DetailView'; import { ErrorDistribution } from './Distribution'; +import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -88,24 +89,10 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { } }, [serviceName, start, end, groupId, uiFilters]); - const { data: errorDistributionData } = useFetcher(() => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - groupId, - uiFilters: JSON.stringify(uiFilters), - }, - }, - }); - } - }, [serviceName, start, end, groupId, uiFilters]); + const { errorDistributionData } = useErrorGroupDistributionFetcher({ + serviceName, + groupId, + }); useTrackPageview({ app: 'apm', path: 'error_group_details' }); useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx index 84b72b62248b0..4022caedadaab 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx @@ -6,8 +6,8 @@ import { mount } from 'enzyme'; import React from 'react'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../../context/url_params_context/mock_url_params_context_provider'; import { mockMoment, toJson } from '../../../../../utils/testHelpers'; import { ErrorGroupList } from '../index'; import props from './props.json'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index be1078ea860c3..200a5f467491b 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -19,7 +19,7 @@ import { truncate, unit, } from '../../../../style/variables'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { ManagedTable } from '../../../shared/ManagedTable'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index e2a02a2f3e7ae..71cb8e0e01602 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -16,13 +16,14 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; +import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; interface ErrorGroupOverviewProps { serviceName: string; @@ -30,26 +31,11 @@ interface ErrorGroupOverviewProps { function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, sortField, sortDirection } = urlParams; - - const { data: errorDistributionData } = useFetcher(() => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, - }, - }); - } - }, [serviceName, start, end, uiFilters]); + const { errorDistributionData } = useErrorGroupDistributionFetcher({ + serviceName, + groupId: undefined, + }); const { data: errorGroupListData } = useFetcher(() => { const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; diff --git a/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx index ab4ca1dfbb49d..148e0733b93ca 100644 --- a/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; describe('Home component', () => { it('should render services', () => { diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index ce8f2b0ba611a..839c087305bd8 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { ApmServiceContextProvider } from '../../../../context/apm_service_context'; +import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; import { APMRouteDefinition } from '../../../../application/routes'; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index ac1668a54ab95..10c8417223c77 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { useFetcher } from '../../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../../hooks/use_fetcher'; import { toQuery } from '../../../../shared/Links/url_helpers'; import { Settings } from '../../../Settings'; import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index 3787202f5dee4..6a56dbf40b33b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -27,7 +27,7 @@ import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { ChartWrapper } from '../ChartWrapper'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 237d33a6a89a3..9fdb34935fee5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, EuiIconTip, } from '@elastic/eui'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { useUxQuery } from '../hooks/useUxQuery'; import { formatToSec } from '../UXMetrics/KeyUXMetrics'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx index 4c4f7110cafb9..2e6c5c8e23ee5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -16,8 +16,8 @@ import { } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { CsmSharedContext } from '../CsmSharedContext'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 4b94b98704da7..d7bc94e6564f1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -6,8 +6,8 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; import { PageLoadDistChart } from '../Charts/PageLoadDistChart'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index c3f4ab44179fe..5c545a63d6d05 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { PercentileRange } from './index'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 84668f4b06d77..b339cc7774d75 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -6,8 +6,8 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; import { PageViewsChart } from '../Charts/PageViewsChart'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index 6c7e2e22a9893..8d759d80352d7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; import { EnvironmentFilter } from '../../../shared/EnvironmentFilter'; import { ServiceNameFilter } from '../URLFilter/ServiceNameFilter'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { RUM_AGENT_NAMES } from '../../../../../common/agent_name'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { UserPercentile } from '../UserPercentile'; export function MainFilters() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index e4e9109f007e7..c810bd3e7c489 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -20,7 +20,7 @@ import { PageLoadAndViews } from './Panels/PageLoadAndViews'; import { VisitorBreakdownsPanel } from './Panels/VisitorBreakdowns'; import { useBreakPoints } from './hooks/useBreakPoints'; import { getPercentileLabel } from './UXMetrics/translations'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; export function RumDashboard() { const { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx index b70621b1e4cbc..756014004cc9b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx @@ -8,7 +8,7 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx index abafdf089748b..a492938deffab 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { createMemoryHistory } from 'history'; -import * as fetcherHook from '../../../../../../hooks/useFetcher'; +import * as fetcherHook from '../../../../../../hooks/use_fetcher'; import { SelectableUrlList } from '../SelectableUrlList'; import { render } from '../../../utils/test_helper'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx index 67692a9a8554b..61f75a430706c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -8,8 +8,8 @@ import useDebounce from 'react-use/lib/useDebounce'; import React, { useEffect, useState, FormEvent, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { EuiTitle } from '@elastic/eui'; -import { useUrlParams } from '../../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../../hooks/use_fetcher'; import { I18LABELS } from '../../translations'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx index ef829ebf7f0cf..655cdaaca933b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx @@ -10,9 +10,9 @@ import { useHistory } from 'react-router-dom'; import { omit } from 'lodash'; import { URLSearch } from './URLSearch'; import { UrlList } from './UrlList'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { removeUndefinedProps } from '../../../../context/UrlParamsContext/helpers'; +import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; export function URLFilter() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index 2ded35deb58f2..690595caa6c0e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -20,7 +20,7 @@ import { TBT_LABEL, TBT_TOOLTIP, } from './translations'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/useUxQuery'; import { UXMetrics } from '../../../../../../observability/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx index 3a6323a747a70..baa9cb7dd74f9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import * as fetcherHook from '../../../../../hooks/useFetcher'; +import * as fetcherHook from '../../../../../hooks/use_fetcher'; import { KeyUXMetrics } from '../KeyUXMetrics'; describe('KeyUXMetrics', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 95a42ce3018f1..392b42cba12e5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -15,11 +15,11 @@ import { } from '@elastic/eui'; import { I18LABELS } from '../translations'; import { KeyUXMetrics } from './KeyUXMetrics'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/useUxQuery'; import { CoreVitals } from '../../../../../../observability/public'; import { CsmSharedContext } from '../CsmSharedContext'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getPercentileLabel } from './translations'; export function UXMetrics() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx index 04c7e3cc00287..260c775c7129b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect } from 'react'; import { EuiSelect } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index ce9485690b930..77d5697c31750 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart'; import { I18LABELS, VisitorBreakdownLabel } from '../translations'; -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; export function VisitorBreakdown() { const { urlParams, uiFilters } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index 3a5c3d80ca7d1..eff03c58e9991 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -21,7 +21,7 @@ import { isErrorEmbeddable, } from '../../../../../../../../src/plugins/embeddable/public'; import { useLayerList } from './useLayerList'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { RenderTooltipContentParams } from '../../../../../../maps/public'; import { MapToolTip } from './MapToolTip'; import { useMapFilters } from './useMapFilters'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index a1cdf7bb646e5..54bfa81f26add 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -22,7 +22,7 @@ import { } from '../../../../../../maps/common/constants'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { SERVICE_NAME, TRANSACTION_TYPE, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts index 774ac23d23196..c5cf081311f66 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FieldFilter as Filter } from '../../../../../../../../src/plugins/data/common'; import { CLIENT_GEO_COUNTRY_ISO_CODE, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts index 16396dc9fc15b..c8cd2c2c64da8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; export function useUxQuery() { const { urlParams, uiFilters } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx index 5522cad5690bc..d5b8cd83d437c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx @@ -13,7 +13,7 @@ import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history'; import { EuiThemeProvider } from '../../../../../../observability/public'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; +import { UrlParamsProvider } from '../../../../context/url_params_context/url_params_context'; export const core = ({ http: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx index 1187b71dff825..659f9f63d0cfa 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx @@ -9,7 +9,7 @@ import { render } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; import { ThemeContext } from 'styled-components'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { Controls } from './Controls'; import { CytoscapeContext } from './Cytoscape'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index b4408e20c04d2..a23fa72314aed 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -8,9 +8,9 @@ import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 8a76c5f7bd8f1..1dea95d369966 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -16,7 +16,7 @@ import React, { useRef, useState, } from 'react'; -import { useTheme } from '../../../hooks/useTheme'; +import { useTheme } from '../../../hooks/use_theme'; import { getCytoscapeOptions } from './cytoscape_options'; import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 63a9cf985959f..07e88294caadb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { CytoscapeContext } from './Cytoscape'; -import { useTheme } from '../../../hooks/useTheme'; +import { useTheme } from '../../../hooks/use_theme'; const EmptyBannerContainer = styled.div` margin: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 788e5f25b6310..36c0c9f37f9c7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -18,7 +18,7 @@ import { getServiceHealthStatus, getServiceHealthStatusColor, } from '../../../../../common/service_health_status'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../../common/utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx index f98a7a1b33dd8..d9f33b8fca4cd 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx @@ -7,7 +7,7 @@ import React, { ReactNode } from 'react'; import { Buttons } from './Buttons'; import { render } from '@testing-library/react'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; function Wrapper({ children }: { children?: ReactNode }) { return <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx index 8670cf623c253..56110a89ed888 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx @@ -9,8 +9,8 @@ import { EuiButton, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { MouseEvent } from 'react'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getAPMHref } from '../../../shared/Links/apm/APMLink'; import { APMQueryParams } from '../../../shared/Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index 70eb5eaf8e576..313b262508c61 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -8,8 +8,8 @@ import cytoscape from 'cytoscape'; import { HttpSetup } from 'kibana/public'; import React, { ComponentType } from 'react'; import { EuiThemeProvider } from '../../../../../../observability/public'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; import { CytoscapeContext } from '../Cytoscape'; import { Popover } from './'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index be8c5cf8cd435..3b737c6fa4170 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -15,8 +15,8 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import { ServiceNodeStats } from '../../../../../common/service_map'; import { ServiceStatsList } from './ServiceStatsList'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { AnomalyDetection } from './AnomalyDetection'; import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index 7b7e3b46bb317..036d02531f794 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -15,7 +15,7 @@ import React, { } from 'react'; import { EuiPopover } from '@elastic/eui'; import cytoscape from 'cytoscape'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { CytoscapeContext } from '../Cytoscape'; import { getAnimationOptions } from '../cytoscape_options'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts index d8a8a3c8e9ab4..e2a54f6048682 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts @@ -15,7 +15,7 @@ import { getServiceHealthStatusColor, ServiceHealthStatus, } from '../../../../common/service_health_status'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { defaultIcon, iconForNode } from './icons'; export const popoverWidth = 280; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx index ae27d4d3baf75..a1fb7e7077add 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx @@ -7,7 +7,7 @@ import { act, waitFor } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { renderWithTheme } from '../../../utils/testHelpers'; import { CytoscapeContext } from './Cytoscape'; import { EmptyBanner } from './EmptyBanner'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index 2a5b4ce44ff46..97e507d7cc871 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -11,9 +11,9 @@ import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; import { EuiThemeProvider } from '../../../../../observability/public'; import { FETCH_STATUS } from '../../../../../observability/public/hooks/use_fetcher'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { LicenseContext } from '../../../context/LicenseContext'; -import * as useFetcherModule from '../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { LicenseContext } from '../../../context/license/license_context'; +import * as useFetcherModule from '../../../hooks/use_fetcher'; import { ServiceMap } from './'; const KibanaReactContext = createKibanaReactContext({ diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 1731d3f9430d4..48a7f8f77ab84 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -13,10 +13,10 @@ import { isActivePlatinumLicense, SERVICE_MAP_TIMEOUT_ERROR, } from '../../../../common/service_map'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useLicense } from '../../../hooks/useLicense'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useLicenseContext } from '../../../context/license/use_license_context'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { DatePicker } from '../../shared/DatePicker'; import { LicensePrompt } from '../../shared/LicensePrompt'; @@ -70,7 +70,7 @@ export function ServiceMap({ serviceName, }: PropsWithChildren<ServiceMapProps>) { const theme = useTheme(); - const license = useLicense(); + const license = useLicenseContext(); const { urlParams } = useUrlParams(); const { data = { elements: [] }, status, error } = useFetcher(() => { diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 74e7b652d0ebe..c4c227deb6918 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -21,8 +21,8 @@ import { asInteger, asPercent, } from '../../../../common/utils/formatters'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { px, truncate, unit } from '../../../style/variables'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 7c0869afe0cd1..18067a43861bd 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -21,7 +21,7 @@ import { omitAllOption, getOptionLabel, } from '../../../../../../../common/agent_configuration/all_option'; -import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; import { FormRowSelect } from './FormRowSelect'; import { APMLink } from '../../../../../shared/Links/apm/APMLink'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx index 54440559070ad..7e1146596dd87 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx @@ -32,8 +32,8 @@ import { validateSetting, } from '../../../../../../../common/agent_configuration/setting_definitions'; import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; -import { FETCH_STATUS } from '../../../../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; import { saveConfig } from './saveConfig'; import { SettingFormRow } from './SettingFormRow'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index db3f2c374a1ae..5ca643428e49c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -14,13 +14,13 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { HttpSetup } from 'kibana/public'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; -import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; import { AgentConfigurationCreateEdit } from './index'; import { ApmPluginContext, ApmPluginContextValue, -} from '../../../../../context/ApmPluginContext'; +} from '../../../../../context/apm_plugin/apm_plugin_context'; import { EuiThemeProvider } from '../../../../../../../observability/public'; storiesOf( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 4f94f255a4e4c..998175c895557 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -14,7 +14,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../../../common/agent_configuration/configuration_types'; -import { FetcherResult } from '../../../../../hooks/useFetcher'; +import { FetcherResult } from '../../../../../hooks/use_fetcher'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; import { ServicePage } from './ServicePage/ServicePage'; import { SettingsPage } from './SettingsPage/SettingsPage'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index adae50db85ada..958aafa8159df 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -13,7 +13,7 @@ import { APIReturnType, callApmApi, } from '../../../../../services/rest/createCallApmApi'; -import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index 81079d78a148a..be4edbe2ea270 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -18,9 +18,9 @@ import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; -import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; -import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { createAgentConfigurationHref, diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 12c63f8702f25..c408d5e960cf3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -16,8 +16,8 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../../observability/public'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; import { AgentConfigurationList } from './List'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx index 53794ca9965ff..2adf85181886c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -7,8 +7,8 @@ import { render } from '@testing-library/react'; import React from 'react'; import { ApmIndices } from '.'; -import * as hooks from '../../../../hooks/useFetcher'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import * as hooks from '../../../../hooks/use_fetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; describe('ApmIndices', () => { it('should not get stuck in infinite loop', () => { diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index a1ef9ddd87271..5a5d20cde9ade 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -19,10 +19,10 @@ import { EuiButton, EuiButtonEmpty, } from '@elastic/eui'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { clearCache } from '../../../../services/rest/callApi'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; const APM_INDEX_LABELS = [ { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx index 5014584c3928a..ffcb85384642a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx @@ -10,7 +10,7 @@ import { NotificationsStart } from 'kibana/public'; import React, { useState } from 'react'; import { px, unit } from '../../../../../../style/variables'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; interface Props { onDelete: () => void; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index c6566af3a8b61..f9c5aa17e411a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -15,7 +15,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; import { FiltersSection } from './FiltersSection'; import { FlyoutFooter } from './FlyoutFooter'; import { LinkSection } from './LinkSection'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 96a634828f669..1da7d415b5660 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -14,15 +14,15 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import * as apmApi from '../../../../../services/rest/createCallApmApi'; import { License } from '../../../../../../../licensing/common/license'; -import * as hooks from '../../../../../hooks/useFetcher'; -import { LicenseContext } from '../../../../../context/LicenseContext'; +import * as hooks from '../../../../../hooks/use_fetcher'; +import { LicenseContext } from '../../../../../context/license/license_context'; import { CustomLinkOverview } from '.'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; const data = [ { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 771a8c6154dc0..6b5c7d583ee8a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -17,8 +17,8 @@ import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; -import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; -import { useLicense } from '../../../../../hooks/useLicense'; +import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; +import { useLicenseContext } from '../../../../../context/license/use_license_context'; import { LicensePrompt } from '../../../../shared/LicensePrompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; @@ -26,7 +26,7 @@ import { CustomLinkTable } from './CustomLinkTable'; import { EmptyPrompt } from './EmptyPrompt'; export function CustomLinkOverview() { - const license = useLicense(); + const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); diff --git a/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx index 21da12477b024..cfef7ca937f66 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx @@ -5,7 +5,7 @@ */ import { render } from '@testing-library/react'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import React, { ReactNode } from 'react'; import { Settings } from './'; import { createMemoryHistory } from 'history'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index ccc1778e9fbde..e709c7e104472 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -21,8 +21,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { createJobs } from './create_jobs'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 9f43f4aa3df91..addfd64a9ef62 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -9,12 +9,12 @@ import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { LicensePrompt } from '../../../shared/LicensePrompt'; -import { useLicense } from '../../../../hooks/useLicense'; +import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>; @@ -27,7 +27,7 @@ const DEFAULT_VALUE: AnomalyDetectionApiResponse = { export function AnomalyDetection() { const plugin = useApmPluginContext(); const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; - const license = useLicense(); + const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const [viewAddEnvironments, setViewAddEnvironments] = useState(false); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 137dcfcdbb4f0..8d6a0740a8a08 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx index 1844e5754cfba..9e21eb2ffc870 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiButton } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref } from '../../../../../../ml/public'; export function LegacyJobsCallout() { diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index c9c577285ee80..e974f05fbe994 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,7 +16,7 @@ import React, { ReactNode } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { HomeLink } from '../../shared/Links/apm/HomeLink'; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index c1c9edb01eb8e..3f325f17af82d 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -8,8 +8,8 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url'; import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url'; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx index e7c0400290dcb..c07e00ef387c9 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx @@ -8,13 +8,13 @@ import { shallow } from 'enzyme'; import React, { ReactNode } from 'react'; import { MemoryRouter, RouteComponentProps } from 'react-router-dom'; import { TraceLink } from './'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import * as hooks from '../../../hooks/useFetcher'; -import * as urlParamsHooks from '../../../hooks/useUrlParams'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import * as hooks from '../../../hooks/use_fetcher'; +import * as urlParamsHooks from '../../../context/url_params_context/use_url_params'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cbab2c44132f3..ab10d6b4f46a0 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 003f2ed05b09e..bebd5bdabbae3 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -27,8 +27,8 @@ import { ValuesType } from 'utility-types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { useTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index 48413d6207ee3..43732c23aea64 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -10,7 +10,7 @@ import { Location } from 'history'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata'; import { WaterfallContainer } from './WaterfallContainer'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx index e3ba02ce42c2e..5217c2abb11dc 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -9,7 +9,7 @@ import { MemoryRouter } from 'react-router-dom'; import { EuiThemeProvider } from '../../../../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { WaterfallContainer } from './index'; import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; import { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 501ca6d33d5af..806350679df55 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -6,7 +6,7 @@ import { Location } from 'history'; import React from 'react'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../../../context/url_params_context/types'; import { ServiceLegends } from './ServiceLegends'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; import { Waterfall } from './Waterfall'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index f78fe39120d8d..0f6bebd9037cc 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -5,7 +5,7 @@ */ import { Location } from 'history'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../../../context/url_params_context/types'; export const location = { pathname: '/services/opbeans-go/transactions/view', diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index c9420dbb81cb9..d90fe393c94a4 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -18,7 +18,7 @@ import { Location } from 'history'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 8f335ddc71c72..c491b9f0e1eff 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -17,19 +17,19 @@ import React, { useMemo } from 'react'; import { isEmpty, flatten } from 'lodash'; import { useHistory } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom'; -import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; -import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; -import { useWaterfall } from '../../../hooks/useWaterfall'; +import { useTransactionChartsFetcher } from '../../../hooks/use_transaction_charts_fetcher'; +import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; +import { useWaterfallFetcher } from './use_waterfall_fetcher'; import { ApmHeader } from '../../shared/ApmHeader'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { TransactionDistribution } from './Distribution'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { Correlations } from '../Correlations'; @@ -50,18 +50,20 @@ export function TransactionDetails({ const { urlParams } = useUrlParams(); const history = useHistory(); const { - data: distributionData, - status: distributionStatus, - } = useTransactionDistribution(urlParams); + distributionData, + distributionStatus, + } = useTransactionDistributionFetcher(); const { - data: transactionChartsData, - status: transactionChartsStatus, - } = useTransactionCharts(); + transactionChartsData, + transactionChartsStatus, + } = useTransactionChartsFetcher(); - const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall( - urlParams - ); + const { + waterfall, + exceedsMax, + status: waterfallStatus, + } = useWaterfallFetcher(); const { transactionName, transactionType } = urlParams; useTrackPageview({ app: 'apm', path: 'transaction_details' }); diff --git a/x-pack/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/useWaterfall.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts index 6264ec45088a2..7458fa79bd1f3 100644 --- a/x-pack/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts @@ -5,9 +5,9 @@ */ import { useMemo } from 'react'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; -import { getWaterfall } from '../components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { getWaterfall } from './WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; const INITIAL_DATA = { root: undefined, @@ -15,7 +15,8 @@ const INITIAL_DATA = { errorsPerTransaction: {}, }; -export function useWaterfall(urlParams: IUrlParams) { +export function useWaterfallFetcher() { + const { urlParams } = useUrlParams(); const { traceId, start, end, transactionId } = urlParams; const { data = INITIAL_DATA, status, error } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 003bd6ba4c122..ae0dd85b6a8b5 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { enableServiceOverview } from '../../../../common/ui_settings_keys'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink'; import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink'; import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink'; @@ -23,7 +23,7 @@ import { ServiceMetrics } from '../service_metrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface Tab { key: string; @@ -44,7 +44,7 @@ interface Props { } export function ServiceDetailTabs({ serviceName, tab }: Props) { - const { agentName } = useApmService(); + const { agentName } = useApmServiceContext(); const { uiSettings } = useApmPluginContext().core; const overviewTab = { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx index e8ad3e65b1a47..3b97849b07790 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx @@ -10,7 +10,7 @@ import { getServiceHealthStatusLabel, ServiceHealthStatus, } from '../../../../../common/service_health_status'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; export function HealthBadge({ healthStatus, diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx index 39cb73d2a0dd9..1c6fa9fe0447e 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx @@ -7,7 +7,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ServiceList, SERVICE_COLUMNS } from './'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 3c84b3982642d..b1d725bba0ca9 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -17,17 +17,17 @@ import url from 'url'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { useAnomalyDetectionJobs } from '../../../hooks/useAnomalyDetectionJobs'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; import { Correlations } from '../Correlations'; import { NoServicesMessage } from './no_services_message'; import { ServiceList } from './ServiceList'; import { MLCallout } from './ServiceList/MLCallout'; +import { useAnomalyDetectionJobsFetcher } from './use_anomaly_detection_jobs_fetcher'; const initialData = { items: [], @@ -37,12 +37,10 @@ const initialData = { let hasDisplayedToast = false; -export function ServiceInventory() { +function useServicesFetcher() { + const { urlParams, uiFilters } = useUrlParams(); const { core } = useApmPluginContext(); - const { - urlParams: { start, end }, - uiFilters, - } = useUrlParams(); + const { start, end } = urlParams; const { data = initialData, status } = useFetcher( (callApmApi) => { if (start && end) { @@ -92,6 +90,13 @@ export function ServiceInventory() { } }, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]); + return { servicesData: data, servicesStatus: status }; +} + +export function ServiceInventory() { + const { core } = useApmPluginContext(); + const { servicesData, servicesStatus } = useServicesFetcher(); + // The page is called "service inventory" to avoid confusion with the // "service overview", but this is tracked in some dashboards because it's the // initial landing page for APM, so it stays as "services_overview" (plural.) @@ -110,9 +115,9 @@ export function ServiceInventory() { ); const { - data: anomalyDetectionJobsData, - status: anomalyDetectionJobsStatus, - } = useAnomalyDetectionJobs(); + anomalyDetectionJobsData, + anomalyDetectionJobsStatus, + } = useAnomalyDetectionJobsFetcher(); const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( 'apm.userHasDismissedServiceInventoryMlCallout', @@ -148,11 +153,11 @@ export function ServiceInventory() { <EuiFlexItem> <EuiPanel> <ServiceList - items={data.items} + items={servicesData.items} noItemsMessage={ <NoServicesMessage - historicalDataFound={data.hasHistoricalData} - status={status} + historicalDataFound={servicesData.hasHistoricalData} + status={servicesStatus} /> } /> diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx index 0fc2a2b4cdcef..cf1ccfbd36aaf 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx @@ -6,8 +6,8 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { NoServicesMessage } from './no_services_message'; function Wrapper({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx index d2763c6632c65..b20efc440312c 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { KibanaLink } from '../../shared/Links/KibanaLink'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ErrorStatePrompt } from '../../shared/ErrorStatePrompt'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index de5e92664a769..1c838a01d05c7 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -13,17 +13,17 @@ import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { ServiceHealthStatus } from '../../../../common/service_health_status'; import { ServiceInventory } from '.'; import { EuiThemeProvider } from '../../../../../observability/public'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import * as useAnomalyDetectionJobs from '../../../hooks/useAnomalyDetectionJobs'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters'; -import * as useDynamicIndexPatternHooks from '../../../hooks/useDynamicIndexPattern'; +import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; import { SessionStorageMock } from '../../../services/__test__/SessionStorageMock'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import * as hook from './use_anomaly_detection_jobs_fetcher'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, @@ -80,19 +80,13 @@ describe('ServiceInventory', () => { status: FETCH_STATUS.SUCCESS, }); - jest - .spyOn(useAnomalyDetectionJobs, 'useAnomalyDetectionJobs') - .mockReturnValue({ - status: FETCH_STATUS.SUCCESS, - data: { - jobs: [], - hasLegacyJobs: false, - }, - refetch: () => undefined, - }); + jest.spyOn(hook, 'useAnomalyDetectionJobsFetcher').mockReturnValue({ + anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, + anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, + }); jest - .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPattern') + .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPatternFetcher') .mockReturnValue({ indexPattern: undefined, status: FETCH_STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts similarity index 50% rename from x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts rename to x-pack/plugins/apm/public/components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts index bef016e5bedd1..901841ac4d593 100644 --- a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts +++ b/x-pack/plugins/apm/public/components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useFetcher } from '../../../hooks/use_fetcher'; -import { useFetcher } from './useFetcher'; - -export function useAnomalyDetectionJobs() { - return useFetcher( +export function useAnomalyDetectionJobsFetcher() { + const { data, status } = useFetcher( (callApmApi) => - callApmApi({ - endpoint: `GET /api/apm/settings/anomaly-detection/jobs`, - }), + callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }), [], { showToastOnError: false } ); + + return { anomalyDetectionJobsData: data, anomalyDetectionJobsStatus: status }; } diff --git a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx index d0f8fc1e61332..bf99f5c87fa6a 100644 --- a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx @@ -13,10 +13,10 @@ import { EuiFlexGroup, } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; +import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; import { MetricsChart } from '../../shared/charts/metrics_chart'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; @@ -31,7 +31,9 @@ export function ServiceMetrics({ serviceName, }: ServiceMetricsProps) { const { urlParams } = useUrlParams(); - const { data, status } = useServiceMetricCharts(urlParams, agentName); + const { data, status } = useServiceMetricChartsFetcher({ + serviceNodeName: undefined, + }); const { start, end } = urlParams; const localFiltersConfig: React.ComponentProps< diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx index c6f7e68e4f4d0..0ba45fae15fef 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ServiceNodeMetrics } from '.'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { RouteComponentProps } from 'react-router-dom'; describe('ServiceNodeMetrics', () => { diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 11de40b47ff86..aa1d9cccbdfa6 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -22,11 +22,11 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { px, truncate, unit } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; import { MetricsChart } from '../../shared/charts/metrics_chart'; @@ -58,12 +58,8 @@ type ServiceNodeMetricsProps = RouteComponentProps<{ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, serviceNodeName } = match.params; - const { agentName } = useApmService(); - const { data } = useServiceMetricCharts( - urlParams, - agentName, - serviceNodeName - ); + const { agentName } = useApmServiceContext(); + const { data } = useServiceMetricChartsFetcher({ serviceNodeName }); const { start, end } = urlParams; const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 15125128d9781..dcb407d27e690 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index b364f027538a6..949f5cce0a64f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -8,17 +8,17 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from 'src/core/public'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; -import * as useDynamicIndexPatternHooks from '../../../hooks/useDynamicIndexPattern'; -import * as useFetcherHooks from '../../../hooks/useFetcher'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import * as useAnnotationsHooks from '../../../hooks/use_annotations'; -import * as useTransactionBreakdownHooks from '../../../hooks/use_transaction_breakdown'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; +import * as useFetcherHooks from '../../../hooks/use_fetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context'; +import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_breakdown_chart/use_transaction_breakdown'; import { renderWithTheme } from '../../../utils/testHelpers'; import { ServiceOverview } from './'; @@ -56,10 +56,10 @@ function Wrapper({ children }: { children?: ReactNode }) { describe('ServiceOverview', () => { it('renders', () => { jest - .spyOn(useAnnotationsHooks, 'useAnnotations') + .spyOn(useAnnotationsHooks, 'useAnnotationsContext') .mockReturnValue({ annotations: [] }); jest - .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPattern') + .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPatternFetcher') .mockReturnValue({ indexPattern: undefined, status: FETCH_STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index b4228878dd9f5..6e183924a80a7 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -14,8 +14,8 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import styled from 'styled-components'; import { asInteger } from '../../../../../common/utils/formatters'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { px, truncate, unit } from '../../../../style/variables'; import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 94d92bfbe89dd..1662f44d1e421 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -9,10 +9,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { asTransactionRate } from '../../../../common/utils/formatters'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; @@ -24,7 +24,7 @@ export function ServiceOverviewThroughputChart({ const theme = useTheme(); const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); - const { transactionType } = useApmService(); + const { transactionType } = useApmServiceContext(); const { start, end } = urlParams; const { data, status } = useFetcher(() => { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index e241bc2fed05a..6b02a44dcc2f4 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -21,8 +21,8 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { px, truncate, unit } from '../../../../style/variables'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType, callApmApi, diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx index 953397b9f3d5f..c14c31afe0445 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx @@ -7,7 +7,7 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { TransactionList } from './'; type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>['items'][0]; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 28a27c034265a..9ff4ad916b174 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -23,10 +23,10 @@ import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; -import { useTransactionList } from '../../../hooks/useTransactionList'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { IUrlParams } from '../../../context/url_params_context/types'; +import { useTransactionChartsFetcher } from '../../../hooks/use_transaction_charts_fetcher'; +import { useTransactionListFetcher } from './use_transaction_list'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; @@ -37,7 +37,7 @@ import { Correlations } from '../Correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { UserExperienceCallout } from './user_experience_callout'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; function getRedirectLocation({ location, @@ -68,22 +68,22 @@ interface TransactionOverviewProps { export function TransactionOverview({ serviceName }: TransactionOverviewProps) { const location = useLocation(); const { urlParams } = useUrlParams(); - const { transactionType, transactionTypes } = useApmService(); + const { transactionType, transactionTypes } = useApmServiceContext(); // redirect to first transaction type useRedirect(getRedirectLocation({ location, transactionType, urlParams })); const { - data: transactionCharts, - status: transactionChartsStatus, - } = useTransactionCharts(); + transactionChartsData, + transactionChartsStatus, + } = useTransactionChartsFetcher(); useTrackPageview({ app: 'apm', path: 'transaction_overview' }); useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); const { - data: transactionListData, - status: transactionListStatus, - } = useTransactionList(urlParams); + transactionListData, + transactionListStatus, + } = useTransactionListFetcher(); const localFiltersConfig: React.ComponentProps< typeof LocalUIFilters @@ -134,7 +134,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { )} <TransactionCharts fetchStatus={transactionChartsStatus} - charts={transactionCharts} + charts={transactionChartsData} urlParams={urlParams} /> <EuiSpacer size="s" /> diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index d4a8b3a46991c..93d56ea19024e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -10,13 +10,13 @@ import { CoreStart } from 'kibana/public'; import React from 'react'; import { Router } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { ApmServiceContextProvider } from '../../../context/apm_service_context'; -import { UrlParamsProvider } from '../../../context/UrlParamsContext'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import * as useFetcherHook from '../../../hooks/useFetcher'; -import * as useServiceTransactionTypesHook from '../../../hooks/use_service_transaction_types'; -import * as useServiceAgentNameHook from '../../../hooks/use_service_agent_name'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context'; +import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context'; +import { IUrlParams } from '../../../context/url_params_context/types'; +import * as useFetcherHook from '../../../hooks/use_fetcher'; +import * as useServiceTransactionTypesHook from '../../../context/apm_service/use_service_transaction_types_fetcher'; +import * as useServiceAgentNameHook from '../../../context/apm_service/use_service_agent_name_fetcher'; import { disableConsoleWarning, renderWithTheme, @@ -46,15 +46,17 @@ function setup({ // mock transaction types jest - .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') + .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypesFetcher') .mockReturnValue(serviceTransactionTypes); // mock agent - jest.spyOn(useServiceAgentNameHook, 'useServiceAgentName').mockReturnValue({ - agentName: 'nodejs', - error: undefined, - status: useFetcherHook.FETCH_STATUS.SUCCESS, - }); + jest + .spyOn(useServiceAgentNameHook, 'useServiceAgentNameFetcher') + .mockReturnValue({ + agentName: 'nodejs', + error: undefined, + status: useFetcherHook.FETCH_STATUS.SUCCESS, + }); jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/useTransactionList.ts rename to x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts index 92b54beb715db..78883ec2cf0d3 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts @@ -5,10 +5,9 @@ */ import { useParams } from 'react-router-dom'; -import { useUiFilters } from '../context/UrlParamsContext'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { APIReturnType } from '../services/rest/createCallApmApi'; -import { useFetcher } from './useFetcher'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>; @@ -18,10 +17,10 @@ const DEFAULT_RESPONSE: Partial<TransactionsAPIResponse> = { bucketSize: 0, }; -export function useTransactionList(urlParams: IUrlParams) { +export function useTransactionListFetcher() { + const { urlParams, uiFilters } = useUrlParams(); const { serviceName } = useParams<{ serviceName?: string }>(); const { transactionType, start, end } = urlParams; - const uiFilters = useUiFilters(urlParams); const { data = DEFAULT_RESPONSE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { @@ -43,8 +42,8 @@ export function useTransactionList(urlParams: IUrlParams) { ); return { - data, - status, - error, + transactionListData: data, + transactionListStatus: status, + transactionListError: error, }; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx index 41e84d4acfba5..6e1154a458d6e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; export function UserExperienceCallout() { const { core } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx index 56501d8c916f4..dd88b1ea7eb73 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx @@ -8,8 +8,8 @@ import { EuiTitle } from '@elastic/eui'; import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { HttpSetup } from '../../../../../../../src/core/public'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { ApmHeader } from './'; diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index a806a3ea60154..04e03cda6a61e 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; const HeaderFlexGroup = styled(EuiFlexGroup)` diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx index 520cc2f423ddd..222c27cc7ed6d 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx @@ -10,12 +10,12 @@ import { mount } from 'enzyme'; import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; import { Router } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { UrlParamsContext, useUiFilters, -} from '../../../context/UrlParamsContext'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; +} from '../../../context/url_params_context/url_params_context'; +import { IUrlParams } from '../../../context/url_params_context/types'; import { DatePicker } from './'; const history = createMemoryHistory(); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index f35cc06748911..f847ce0b6e96f 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -8,8 +8,8 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { clearCache } from '../../../services/rest/callApi'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings'; diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index cace4c2770f37..4522cfa7195fd 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -13,8 +13,8 @@ import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, } from '../../../../common/environment_filter_values'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../Links/url_helpers'; function updateEnvironmentUrl( @@ -67,7 +67,7 @@ export function EnvironmentFilter() { const { environment } = uiFilters; const { start, end } = urlParams; - const { environments, status = 'loading' } = useEnvironments({ + const { environments, status = 'loading' } = useEnvironmentsFetcher({ serviceName, start, end, diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index e7dd03db6b63c..2276704edc342 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -13,7 +13,7 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { UIProcessorEvent } from '../../../../common/processor_event'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../context/url_params_context/types'; export function getBoolFilter({ groupId, diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 2ef93fc32200e..5284e3f6aa011 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -14,9 +14,9 @@ import { IIndexPattern, QuerySuggestion, } from '../../../../../../../src/plugins/data/public'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { getBoolFilter } from './get_bool_filter'; // @ts-expect-error @@ -65,7 +65,7 @@ export function KueryBar() { const example = examples[processorEvent || 'defaults']; - const { indexPattern } = useDynamicIndexPattern(processorEvent); + const { indexPattern } = useDynamicIndexPatternFetcher(processorEvent); const placeholder = i18n.translate('xpack.apm.kueryBar.placeholder', { defaultMessage: `Search {event, select, diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx index 1819e71a49753..bd68e7db77714 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx @@ -9,7 +9,7 @@ import { LicensePrompt } from '.'; import { ApmPluginContext, ApmPluginContextValue, -} from '../../../context/ApmPluginContext'; +} from '../../../context/apm_plugin/apm_plugin_context'; const contextMock = ({ core: { http: { basePath: { prepend: () => {} } } }, diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 93b5672aa54f9..70286655bba88 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -12,7 +12,7 @@ import { useLocation } from 'react-router-dom'; import rison, { RisonValue } from 'rison-node'; import url from 'url'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { getTimepickerRisonData } from '../rison_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 8c2829a515f83..e2447cc7a67a5 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -6,7 +6,7 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; // union type constisting of valid guide sections that we link to type DocsSection = '/apm/get-started' | '/x-pack' | '/apm/server' | '/kibana'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx index 630235e54c9fa..6d4bbbbfc2f80 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -9,7 +9,7 @@ import { IBasePath } from 'kibana/public'; import React from 'react'; import url from 'url'; import { InfraAppId } from '../../../../../infra/public'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { fromQuery } from './url_helpers'; interface InfraQueryParams { diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx index 8aa0d4f5a3354..ab44374f48167 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx @@ -7,7 +7,7 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; import url from 'url'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; interface Props extends EuiLinkAnchorProps { path?: string; diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx index 5fbcd475cb47b..7bf017fb239e3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx @@ -6,9 +6,9 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; interface MlRisonData { ml?: { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 0f671fd363c75..eabef034bf3d9 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref } from '../../../../../../ml/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; export function useTimeSeriesExplorerHref({ jobId, diff --git a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx index 0ff73d91d7c5b..68bee36dbe283 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; const SETUP_INSTRUCTIONS_LABEL = i18n.translate( 'xpack.apm.setupInstructionsButtonLabel', diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 41c932bf9c9f5..98046193e3807 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import url from 'url'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams, fromQuery, toQuery } from '../url_helpers'; interface Props extends EuiLinkAnchorProps { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx index 30b91fe2564f1..dcf21de7dca8d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index fbae80203f03b..de7130e878608 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx index 2553ec4353194..afdb177e467d8 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index 0a9553bcbfe6c..c107b436717c2 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index 6aa362707800f..caa1498e6df87 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -11,7 +11,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx index c9b26b557512c..ee798e0208c2b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx index 23e795b026d0c..92ff1b8a68ac0 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx index 039d9dcb1c0ed..318a1590be77c 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx @@ -11,7 +11,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx index e6d266091ae52..43f7b089a2965 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -13,7 +13,7 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../Links/url_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 9db563a0f6ba8..6f62fd24e71ea 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -8,7 +8,7 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { orderBy } from 'lodash'; import React, { ReactNode, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../Links/url_helpers'; // TODO: this should really be imported from EUI diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx index e95122f54aff1..8f44d98cecdf7 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ErrorMetadata } from '..'; import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 1f10d923e351e..c97e506187347 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { SpanMetadata } from '..'; import { Span } from '../../../../../../typings/es_schemas/ui/span'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 8359716fc6966..4080a300ba17f 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { TransactionMetadata } from '..'; import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx index 8e53aa4aa1089..8a4cd588c8260 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MetadataTable } from '..'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; import { SectionsWithRows } from '../helper'; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index 1d2ac4d18a2a7..283433fa37bf9 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { HeightRetainer } from '../HeightRetainer'; import { fromQuery, toQuery } from '../Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index ed33c59af36f4..83c2acb57e3c7 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { EuiBadge } from '@elastic/eui'; -import { useTheme } from '../../../hooks/useTheme'; +import { useTheme } from '../../../hooks/use_theme'; import { px } from '../../../../public/style/variables'; import { units } from '../../../style/variables'; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx index 0241167aba1fb..777200099976e 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx @@ -7,7 +7,7 @@ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx index db7a284f6adff..c4547595645a2 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx @@ -10,8 +10,8 @@ import { MemoryRouter } from 'react-router-dom'; import { CustomLinkMenuSection } from '.'; import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import * as useFetcher from '../../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import * as useFetcher from '../../../../hooks/use_fetcher'; import { expectTextsInDocument, expectTextsNotInDocument, 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 2825363b10197..0a67db0f15b32 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 @@ -22,7 +22,7 @@ import { import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLinkList } from './CustomLinkList'; import { CustomLinkToolbar } from './CustomLinkToolbar'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { LoadingStatePrompt } from '../../LoadingStatePrompt'; import { px } from '../../../../style/variables'; import { CreateEditCustomLinkFlyout } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout'; 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 15a85113406e1..3f74b80bab064 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -18,9 +18,9 @@ import { SectionTitle, } from '../../../../../observability/public'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useLicense } from '../../../hooks/useLicense'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useLicenseContext } from '../../../context/license/use_license_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { CustomLinkMenuSection } from './CustomLinkMenuSection'; import { getSections } from './sections'; @@ -39,7 +39,7 @@ function ActionMenuButton({ onClick }: { onClick: () => void }) { } export function TransactionActionMenu({ transaction }: Props) { - const license = useLicense(); + const license = useLicenseContext(); const hasGoldLicense = license?.isActive && license?.hasAtLeast('gold'); const { core } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 9b5f00f76eeb2..8cb863c8fc385 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { License } from '../../../../../../licensing/common/license'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import * as hooks from '../../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { LicenseContext } from '../../../../context/license/license_context'; +import * as hooks from '../../../../hooks/use_fetcher'; import * as apmApi from '../../../../services/rest/createCallApmApi'; import { expectTextsInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index 4433865b44991..c77de875dc84f 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -10,7 +10,7 @@ import { isEmpty, pickBy } from 'lodash'; import moment from 'moment'; import url from 'url'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../context/url_params_context/types'; import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; import { getInfraHref } from '../Links/InfraLink'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx index 1a2a90c9fb3c3..eebb9e8d23d98 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import styled from 'styled-components'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { fontSizes, px, units } from '../../../../style/variables'; export enum Shape { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index 37d3664e98acd..a6b46f4a64691 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; import styled from 'styled-components'; import { asDuration } from '../../../../../../common/utils/formatters'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { Legend } from '../../Legend'; import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx index abe81185635b5..e69b23cf5f008 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx @@ -8,7 +8,7 @@ import { fireEvent } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, renderWithTheme, diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index de63e2323ddac..c6847bd5e674d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -8,12 +8,12 @@ import React, { useState } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { asDuration } from '../../../../../../common/utils/formatters'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useTheme } from '../../../../../hooks/use_theme'; import { TRACE_ID, TRANSACTION_ID, } from '../../../../../../common/elasticsearch_fieldnames'; -import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { px, unit, units } from '../../../../../style/variables'; import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx index cb5a44432dcbc..dcdfee22e3cfc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx @@ -9,7 +9,7 @@ import { inRange } from 'lodash'; import { Sticky } from 'react-sticky'; import { XAxis, XYPlot } from 'react-vis'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { px } from '../../../../style/variables'; import { Mark } from './'; import { LastTickValue } from './LastTickValue'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx index 5ea2e4cfedf18..ee1c899123994 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { VerticalGridLines, XYPlot } from 'react-vis'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { Mark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks'; import { PlotValues } from './plotUtils'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx index c0e8f869ce647..359eadfc55cff 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx @@ -5,7 +5,7 @@ */ import { render } from '@testing-library/react'; import React from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ChartContainer } from './chart_container'; describe('ChartContainer', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx index b4486f1e9b94a..ef58430e1e31e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx @@ -7,7 +7,7 @@ import { EuiLoadingChart, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; interface Props { hasData: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 9a561571df5a7..506c27051b511 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -15,7 +15,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; import { Maybe } from '../../../../../typings/common'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { TimeseriesChart } from '../timeseries_chart'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx index 3819ed30d104a..3bfcba63685b6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { px, unit } from '../../../../../style/variables'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useTheme } from '../../../../../hooks/use_theme'; import { SparkPlot } from '../'; type Color = diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index a857707ca0c75..689f80e01247e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -29,11 +29,11 @@ import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../observability/public'; import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; import { RectCoordinate, TimeSeries } from '../../../../typings/timeseries'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useAnnotations } from '../../../hooks/use_annotations'; -import { useChartPointerEvent } from '../../../hooks/use_chart_pointer_event'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context'; +import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { AnomalySeries } from '../../../selectors/chart_selectors'; import { unit } from '../../../style/variables'; import { ChartContainer } from './chart_container'; @@ -72,9 +72,9 @@ export function TimeseriesChart({ }: Props) { const history = useHistory(); const chartRef = React.createRef<Chart>(); - const { annotations } = useAnnotations(); + const { annotations } = useAnnotationsContext(); const chartTheme = useChartTheme(); - const { pointerEvent, setPointerEvent } = useChartPointerEvent(); + const { pointerEvent, setPointerEvent } = useChartPointerEventContext(); const { urlParams } = useUrlParams(); const theme = useTheme(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx index 4d9a1637bea76..38a980fbcd90a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useTransactionBreakdown } from '../../../../hooks/use_transaction_breakdown'; +import { useTransactionBreakdown } from './use_transaction_breakdown'; import { TransactionBreakdownChartContents } from './transaction_breakdown_chart_contents'; export function TransactionBreakdownChart({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 20056a6831adf..0eda922519f85 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -28,11 +28,11 @@ import { asPercent, } from '../../../../../common/utils/formatters'; import { TimeSeries } from '../../../../../typings/timeseries'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useTheme } from '../../../../hooks/useTheme'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useAnnotations } from '../../../../hooks/use_annotations'; -import { useChartPointerEvent } from '../../../../hooks/use_chart_pointer_event'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useAnnotationsContext } from '../../../../context/annotations/use_annotations_context'; +import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../charts/chart_container'; import { onBrushEnd } from '../../charts/helper/helper'; @@ -52,9 +52,9 @@ export function TransactionBreakdownChartContents({ }: Props) { const history = useHistory(); const chartRef = React.createRef<Chart>(); - const { annotations } = useAnnotations(); + const { annotations } = useAnnotationsContext(); const chartTheme = useChartTheme(); - const { pointerEvent, setPointerEvent } = useChartPointerEvent(); + const { pointerEvent, setPointerEvent } = useChartPointerEventContext(); const { urlParams } = useUrlParams(); const theme = useTheme(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts similarity index 80% rename from x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts rename to x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index f1671ed7aa6d9..ff744d763ecae 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -5,15 +5,15 @@ */ import { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; -import { useApmService } from './use_apm_service'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; export function useTransactionBreakdown() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const { start, end, transactionName } = urlParams; - const { transactionType } = useApmService(); + const { transactionType } = useApmServiceContext(); const { data = { timeseries: undefined }, error, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index ac117bbbd922a..bb7c0a9104fc7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -20,11 +20,11 @@ import { TRANSACTION_ROUTE_CHANGE, } from '../../../../../common/transaction_types'; import { asTransactionRate } from '../../../../../common/utils/formatters'; -import { AnnotationsContextProvider } from '../../../../context/annotations_context'; -import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event_context'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context'; +import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context'; +import { LicenseContext } from '../../../../context/license/license_context'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { ITransactionChartData } from '../../../../selectors/chart_selectors'; import { TimeseriesChart } from '../timeseries_chart'; import { TransactionBreakdownChart } from '../transaction_breakdown_chart'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index ee5cd8960d335..f0569ea1a0752 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 00472df95c4b1..06a5e7baef79b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useTheme } from '../../../../hooks/useTheme'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { TimeseriesChart } from '../timeseries_chart'; diff --git a/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx index e8d62cd8bd85b..9538d46960fc9 100644 --- a/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx @@ -5,7 +5,7 @@ */ import React, { ReactNode } from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ErrorStatePrompt } from '../ErrorStatePrompt'; export function TableFetchWrapper({ diff --git a/x-pack/plugins/apm/public/context/annotations_context.tsx b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx similarity index 83% rename from x-pack/plugins/apm/public/context/annotations_context.tsx rename to x-pack/plugins/apm/public/context/annotations/annotations_context.tsx index 4e09a3d227b11..77285f976d850 100644 --- a/x-pack/plugins/apm/public/context/annotations_context.tsx +++ b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx @@ -6,10 +6,10 @@ import React, { createContext } from 'react'; import { useParams } from 'react-router-dom'; -import { Annotation } from '../../common/annotations'; -import { useFetcher } from '../hooks/useFetcher'; -import { useUrlParams } from '../hooks/useUrlParams'; -import { callApmApi } from '../services/rest/createCallApmApi'; +import { Annotation } from '../../../common/annotations'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; +import { callApmApi } from '../../services/rest/createCallApmApi'; export const AnnotationsContext = createContext({ annotations: [] } as { annotations: Annotation[]; diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/context/annotations/use_annotations_context.ts similarity index 80% rename from x-pack/plugins/apm/public/hooks/use_annotations.ts rename to x-pack/plugins/apm/public/context/annotations/use_annotations_context.ts index 1cd9a7e65dda2..7fdc602b1916e 100644 --- a/x-pack/plugins/apm/public/hooks/use_annotations.ts +++ b/x-pack/plugins/apm/public/context/annotations/use_annotations_context.ts @@ -5,9 +5,9 @@ */ import { useContext } from 'react'; -import { AnnotationsContext } from '../context/annotations_context'; +import { AnnotationsContext } from './annotations_context'; -export function useAnnotations() { +export function useAnnotationsContext() { const context = useContext(AnnotationsContext); if (!context) { diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx similarity index 94% rename from x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx rename to x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx index 44952e64db59c..0e26db4820ea1 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx @@ -6,7 +6,7 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import { createContext } from 'react'; -import { ConfigSchema } from '../../'; +import { ConfigSchema } from '../..'; import { ApmPluginSetupDeps } from '../../plugin'; export interface ApmPluginContextValue { diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx similarity index 97% rename from x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx rename to x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 25e7f23a00125..7ab46c65c90d9 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -5,7 +5,7 @@ */ import React, { ReactNode } from 'react'; import { Observable, of } from 'rxjs'; -import { ApmPluginContext, ApmPluginContextValue } from '.'; +import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; import { ConfigSchema } from '../..'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/hooks/useApmPluginContext.ts b/x-pack/plugins/apm/public/context/apm_plugin/use_apm_plugin_context.ts similarity index 84% rename from x-pack/plugins/apm/public/hooks/useApmPluginContext.ts rename to x-pack/plugins/apm/public/context/apm_plugin/use_apm_plugin_context.ts index 80a04edbca858..7c480ea3da275 100644 --- a/x-pack/plugins/apm/public/hooks/useApmPluginContext.ts +++ b/x-pack/plugins/apm/public/context/apm_plugin/use_apm_plugin_context.ts @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; -import { ApmPluginContext } from '../context/ApmPluginContext'; +import { ApmPluginContext } from './apm_plugin_context'; export function useApmPluginContext() { return useContext(ApmPluginContext); diff --git a/x-pack/plugins/apm/public/context/apm_service_context.test.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/context/apm_service_context.test.tsx rename to x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx diff --git a/x-pack/plugins/apm/public/context/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx similarity index 75% rename from x-pack/plugins/apm/public/context/apm_service_context.tsx rename to x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 2f1b33dea5aa6..b07763aed7b00 100644 --- a/x-pack/plugins/apm/public/context/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -5,15 +5,15 @@ */ import React, { createContext, ReactNode } from 'react'; -import { isRumAgentName } from '../../common/agent_name'; +import { isRumAgentName } from '../../../common/agent_name'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, -} from '../../common/transaction_types'; -import { useServiceTransactionTypes } from '../hooks/use_service_transaction_types'; -import { useUrlParams } from '../hooks/useUrlParams'; -import { useServiceAgentName } from '../hooks/use_service_agent_name'; -import { IUrlParams } from './UrlParamsContext/types'; +} from '../../../common/transaction_types'; +import { useServiceTransactionTypesFetcher } from './use_service_transaction_types_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; +import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher'; +import { IUrlParams } from '../url_params_context/types'; export const APMServiceContext = createContext<{ agentName?: string; @@ -27,8 +27,8 @@ export function ApmServiceContextProvider({ children: ReactNode; }) { const { urlParams } = useUrlParams(); - const { agentName } = useServiceAgentName(); - const transactionTypes = useServiceTransactionTypes(); + const { agentName } = useServiceAgentNameFetcher(); + const transactionTypes = useServiceTransactionTypesFetcher(); const transactionType = getTransactionType({ urlParams, transactionTypes, diff --git a/x-pack/plugins/apm/public/hooks/use_apm_service.ts b/x-pack/plugins/apm/public/context/apm_service/use_apm_service_context.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/use_apm_service.ts rename to x-pack/plugins/apm/public/context/apm_service/use_apm_service_context.ts index bc80c3771c39d..85c135f36719f 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_service.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_apm_service_context.ts @@ -5,8 +5,8 @@ */ import { useContext } from 'react'; -import { APMServiceContext } from '../context/apm_service_context'; +import { APMServiceContext } from './apm_service_context'; -export function useApmService() { +export function useApmServiceContext() { return useContext(APMServiceContext); } diff --git a/x-pack/plugins/apm/public/hooks/use_service_agent_name.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts similarity index 83% rename from x-pack/plugins/apm/public/hooks/use_service_agent_name.ts rename to x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts index 199f14532f7b4..9a1d969ec2c33 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_agent_name.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts @@ -5,10 +5,10 @@ */ import { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; -export function useServiceAgentName() { +export function useServiceAgentNameFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx similarity index 83% rename from x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx rename to x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx index 9d8892ac79b7d..85a10cc273bac 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx @@ -5,12 +5,12 @@ */ import { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; const INITIAL_DATA = { transactionTypes: [] }; -export function useServiceTransactionTypes() { +export function useServiceTransactionTypesFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/context/chart_pointer_event_context.tsx b/x-pack/plugins/apm/public/context/chart_pointer_event/chart_pointer_event_context.tsx similarity index 100% rename from x-pack/plugins/apm/public/context/chart_pointer_event_context.tsx rename to x-pack/plugins/apm/public/context/chart_pointer_event/chart_pointer_event_context.tsx diff --git a/x-pack/plugins/apm/public/hooks/use_chart_pointer_event.tsx b/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx similarity index 78% rename from x-pack/plugins/apm/public/hooks/use_chart_pointer_event.tsx rename to x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx index 058ec594e2d22..bf53273104d60 100644 --- a/x-pack/plugins/apm/public/hooks/use_chart_pointer_event.tsx +++ b/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx @@ -5,9 +5,9 @@ */ import { useContext } from 'react'; -import { ChartPointerEventContext } from '../context/chart_pointer_event_context'; +import { ChartPointerEventContext } from './chart_pointer_event_context'; -export function useChartPointerEvent() { +export function useChartPointerEventContext() { const context = useContext(ChartPointerEventContext); if (!context) { diff --git a/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/plugins/apm/public/context/license/Invalid_license_notification.tsx similarity index 100% rename from x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx rename to x-pack/plugins/apm/public/context/license/Invalid_license_notification.tsx diff --git a/x-pack/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/plugins/apm/public/context/license/license_context.tsx similarity index 87% rename from x-pack/plugins/apm/public/context/LicenseContext/index.tsx rename to x-pack/plugins/apm/public/context/license/license_context.tsx index e6615a2fc98bf..557f135fa4c0e 100644 --- a/x-pack/plugins/apm/public/context/LicenseContext/index.tsx +++ b/x-pack/plugins/apm/public/context/license/license_context.tsx @@ -7,8 +7,8 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { ILicense } from '../../../../licensing/public'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; -import { InvalidLicenseNotification } from './InvalidLicenseNotification'; +import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; +import { InvalidLicenseNotification } from './Invalid_license_notification'; export const LicenseContext = React.createContext<ILicense | undefined>( undefined diff --git a/x-pack/plugins/apm/public/hooks/useLicense.ts b/x-pack/plugins/apm/public/context/license/use_license_context.ts similarity index 77% rename from x-pack/plugins/apm/public/hooks/useLicense.ts rename to x-pack/plugins/apm/public/context/license/use_license_context.ts index ca828e49706a8..e86bb78d127ab 100644 --- a/x-pack/plugins/apm/public/hooks/useLicense.ts +++ b/x-pack/plugins/apm/public/context/license/use_license_context.ts @@ -5,8 +5,8 @@ */ import { useContext } from 'react'; -import { LicenseContext } from '../context/LicenseContext'; +import { LicenseContext } from './license_context'; -export function useLicense() { +export function useLicenseContext() { return useContext(LicenseContext); } diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts b/x-pack/plugins/apm/public/context/url_params_context/constants.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts rename to x-pack/plugins/apm/public/context/url_params_context/constants.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/url_params_context/helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts rename to x-pack/plugins/apm/public/context/url_params_context/helpers.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx b/x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx similarity index 93% rename from x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx rename to x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx index fd01e057ac3de..b593cbd57a9a9 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { IUrlParams } from './types'; -import { UrlParamsContext, useUiFilters } from '.'; +import { UrlParamsContext, useUiFilters } from './url_params_context'; const defaultUrlParams = { page: 0, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts rename to x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/url_params_context/types.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/types.ts rename to x-pack/plugins/apm/public/context/url_params_context/types.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/url_params_context.test.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx similarity index 98% rename from x-pack/plugins/apm/public/context/UrlParamsContext/url_params_context.test.tsx rename to x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx index 3a6ccce178cd4..6b57039678e0a 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/url_params_context.test.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx @@ -5,7 +5,7 @@ */ import * as React from 'react'; -import { UrlParamsContext, UrlParamsProvider } from './'; +import { UrlParamsContext, UrlParamsProvider } from './url_params_context'; import { mount } from 'enzyme'; import { Location, History } from 'history'; import { MemoryRouter, Router } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx similarity index 98% rename from x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx rename to x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx index 5682009019d7f..0a3f8459ff002 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx @@ -15,7 +15,7 @@ import { withRouter } from 'react-router-dom'; import { uniqueId, mapValues } from 'lodash'; import { IUrlParams } from './types'; import { getParsedDate } from './helpers'; -import { resolveUrlParams } from './resolveUrlParams'; +import { resolveUrlParams } from './resolve_url_params'; import { UIFilters } from '../../../typings/ui_filters'; import { localUIFilterNames, diff --git a/x-pack/plugins/apm/public/hooks/useUrlParams.tsx b/x-pack/plugins/apm/public/context/url_params_context/use_url_params.tsx similarity index 84% rename from x-pack/plugins/apm/public/hooks/useUrlParams.tsx rename to x-pack/plugins/apm/public/context/url_params_context/use_url_params.tsx index b9f47046812be..1bf071d9db35e 100644 --- a/x-pack/plugins/apm/public/hooks/useUrlParams.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/use_url_params.tsx @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; -import { UrlParamsContext } from '../context/UrlParamsContext'; +import { UrlParamsContext } from './url_params_context'; export function useUrlParams() { return useContext(UrlParamsContext); diff --git a/x-pack/plugins/apm/public/hooks/useCallApi.ts b/x-pack/plugins/apm/public/hooks/useCallApi.ts index 3fec36e7fb24b..79e439c3f7e7a 100644 --- a/x-pack/plugins/apm/public/hooks/useCallApi.ts +++ b/x-pack/plugins/apm/public/hooks/useCallApi.ts @@ -6,7 +6,7 @@ import { useMemo } from 'react'; import { callApi } from '../services/rest/callApi'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; import { FetchOptions } from '../../common/fetch_options'; export function useCallApi() { diff --git a/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts index b4a354c231633..66edb84378a45 100644 --- a/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts +++ b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts @@ -5,7 +5,7 @@ */ import url from 'url'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; export function useKibanaUrl( /** The path to the plugin */ path: string, diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index da1797fd712b1..551e92f8ba034 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -15,10 +15,10 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/ui_filters/local_ui_filters/config'; import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; -import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; +import { removeUndefinedProps } from '../context/url_params_context/helpers'; import { useCallApi } from './useCallApi'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; import { LocalUIFilterName } from '../../common/ui_filter'; const getInitialData = ( diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx index dcd6ed0ba4934..9127bd3adc69e 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx @@ -9,11 +9,11 @@ import produce from 'immer'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { routes } from '../components/app/Main/route_config'; -import { ApmPluginContextValue } from '../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../context/ApmPluginContext/MockApmPluginContext'; +} from '../context/apm_plugin/mock_apm_plugin_context'; import { useBreadcrumbs } from './use_breadcrumbs'; function createWrapper(path: string) { diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts index 640170bf3bff2..089381cbe05b5 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts @@ -16,7 +16,7 @@ import { } from 'react-router-dom'; import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes'; import { getAPMHref } from '../components/shared/Links/apm/APMLink'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; interface BreadcrumbWithoutLink extends ChromeBreadcrumb { match: Match<Record<string, string>>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/use_dynamic_index_pattern.ts similarity index 89% rename from x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts rename to x-pack/plugins/apm/public/hooks/use_dynamic_index_pattern.ts index d0e12d8537846..becdf1f9ecc5e 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/use_dynamic_index_pattern.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useFetcher } from './useFetcher'; +import { useFetcher } from './use_fetcher'; import { UIProcessorEvent } from '../../common/processor_event'; -export function useDynamicIndexPattern( +export function useDynamicIndexPatternFetcher( processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx similarity index 94% rename from x-pack/plugins/apm/public/hooks/useEnvironments.tsx rename to x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx index 05ac780aefbde..1ad151b8c7e90 100644 --- a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx +++ b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import { useFetcher } from './useFetcher'; +import { useFetcher } from './use_fetcher'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, @@ -23,7 +23,7 @@ function getEnvironmentOptions(environments: string[]) { return [ENVIRONMENT_ALL, ...environmentOptions]; } -export function useEnvironments({ +export function useEnvironmentsFetcher({ serviceName, start, end, diff --git a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx new file mode 100644 index 0000000000000..1c17be884ebf5 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useFetcher } from './use_fetcher'; + +export function useErrorGroupDistributionFetcher({ + serviceName, + groupId, +}: { + serviceName: string; + groupId: string | undefined; +}) { + const { urlParams, uiFilters } = useUrlParams(); + const { start, end } = urlParams; + const { data } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', + params: { + path: { serviceName }, + query: { + start, + end, + groupId, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, + [serviceName, start, end, groupId, uiFilters] + ); + + return { errorDistributionData: data }; +} diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx index e837851828d94..e6f3b71af8a85 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx @@ -7,8 +7,8 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { delay } from '../utils/testHelpers'; -import { useFetcher } from './useFetcher'; -import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; +import { useFetcher } from './use_fetcher'; +import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context'; const wrapper = MockApmPluginContextWrapper; diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/hooks/useFetcher.test.tsx rename to x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx index 59dd9455c724c..9b4ad6bc9bb51 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx @@ -6,9 +6,9 @@ import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; import { delay } from '../utils/testHelpers'; -import { FetcherResult, useFetcher } from './useFetcher'; -import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; -import { ApmPluginContextValue } from '../context/ApmPluginContext'; +import { FetcherResult, useFetcher } from './use_fetcher'; +import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context'; +import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context'; // Wrap the hook with a provider so it can useApmPluginContext const wrapper = MockApmPluginContextWrapper; diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx similarity index 98% rename from x-pack/plugins/apm/public/hooks/useFetcher.tsx rename to x-pack/plugins/apm/public/hooks/use_fetcher.tsx index 6add0e8a2b480..a9a4871dc8707 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { APMClient, callApmApi } from '../services/rest/createCallApmApi'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; export enum FETCH_STATUS { LOADING = 'loading', diff --git a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts rename to x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts index 7a54c6ffc6dbe..c888c51589563 100644 --- a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts @@ -7,22 +7,23 @@ import { useParams } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent'; -import { useUiFilters } from '../context/UrlParamsContext'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; +import { useFetcher } from './use_fetcher'; const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { charts: [], }; -export function useServiceMetricCharts( - urlParams: IUrlParams, - agentName?: string, - serviceNodeName?: string -) { +export function useServiceMetricChartsFetcher({ + serviceNodeName, +}: { + serviceNodeName: string | undefined; +}) { + const { urlParams, uiFilters } = useUrlParams(); + const { agentName } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end } = urlParams; - const uiFilters = useUiFilters(urlParams); const { data = INITIAL_DATA, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && agentName) { diff --git a/x-pack/plugins/apm/public/hooks/useTheme.tsx b/x-pack/plugins/apm/public/hooks/use_theme.tsx similarity index 100% rename from x-pack/plugins/apm/public/hooks/useTheme.tsx rename to x-pack/plugins/apm/public/hooks/use_theme.tsx diff --git a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts similarity index 82% rename from x-pack/plugins/apm/public/hooks/useTransactionCharts.ts rename to x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts index c790ac57edc3b..f5105e38b985e 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts @@ -7,10 +7,10 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { getTransactionCharts } from '../selectors/chart_selectors'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; -export function useTransactionCharts() { +export function useTransactionChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams: { transactionType, start, end, transactionName }, @@ -45,8 +45,8 @@ export function useTransactionCharts() { ); return { - data: memoizedData, - status, - error, + transactionChartsData: memoizedData, + transactionChartsStatus: status, + transactionChartsError: error, }; } diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts similarity index 89% rename from x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts rename to x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 9cbfee37d1253..74222e8ffe038 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -6,12 +6,11 @@ import { flatten, omit, isEmpty } from 'lodash'; import { useHistory, useParams } from 'react-router-dom'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; -import { useUiFilters } from '../context/UrlParamsContext'; +import { useFetcher } from './use_fetcher'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { maybe } from '../../common/utils/maybe'; import { APIReturnType } from '../services/rest/createCallApmApi'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; @@ -21,8 +20,9 @@ const INITIAL_DATA = { bucketSize: 0, }; -export function useTransactionDistribution(urlParams: IUrlParams) { +export function useTransactionDistributionFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); const { start, end, @@ -31,10 +31,8 @@ export function useTransactionDistribution(urlParams: IUrlParams) { traceId, transactionName, } = urlParams; - const uiFilters = useUiFilters(urlParams); const history = useHistory(); - const { data = INITIAL_DATA, status, error } = useFetcher( async (callApmApi) => { if (serviceName && start && end && transactionType && transactionName) { @@ -96,5 +94,9 @@ export function useTransactionDistribution(urlParams: IUrlParams) { [serviceName, start, end, transactionType, transactionName, uiFilters] ); - return { data, status, error }; + return { + distributionData: data, + distributionStatus: status, + distributionError: error, + }; } diff --git a/x-pack/plugins/apm/public/selectors/chart_selectors.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.ts index 2fdcaf9e4e675..37bd04e5d9980 100644 --- a/x-pack/plugins/apm/public/selectors/chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/chart_selectors.ts @@ -17,7 +17,7 @@ import { RectCoordinate, TimeSeries, } from '../../typings/timeseries'; -import { IUrlParams } from '../context/UrlParamsContext/types'; +import { IUrlParams } from '../context/url_params_context/types'; import { getEmptySeries } from '../components/shared/charts/helper/get_empty_series'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; import { asDuration, asTransactionRate } from '../../common/utils/formatters'; diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 87dfeb95b6326..21c87c18be363 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -24,8 +24,8 @@ import { PromiseReturnType } from '../../../observability/typings/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; import { UIFilters } from '../../typings/ui_filters'; -import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; +import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context'; +import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; const originalConsoleWarn = console.warn; // eslint-disable-line no-console /** From 2ad3d2bc53dc5a6f7d57c382f14aade9f191976a Mon Sep 17 00:00:00 2001 From: Chris Roberson <chrisronline@gmail.com> Date: Wed, 2 Dec 2020 07:23:50 -0500 Subject: [PATCH 053/107] Make alert status fetching more resilient (#84676) --- .../public/views/base_controller.js | 6 +-- .../lib/cluster/get_clusters_from_request.js | 40 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 0eb40c8dd5963..62c15f0913569 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -163,12 +163,12 @@ export class MonitoringViewBaseController { if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { promises.push(updateSetupModeData()); } - this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); + this.updateDataPromise = new PromiseWithCancel(Promise.allSettled(promises)); return this.updateDataPromise.promise().then(([pageData, alerts]) => { $scope.$apply(() => { this._isDataInitialized = true; // render will replace loading screen with the react component - $scope.pageData = this.data = pageData; // update the view's data with the fetch result - $scope.alerts = this.alerts = alerts; + $scope.pageData = this.data = pageData.value; // update the view's data with the fetch result + $scope.alerts = this.alerts = alerts.value || {}; }); }); }; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index ddc33a4b93730..b676abd3de2dd 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -151,20 +151,32 @@ export async function getClustersFromRequest( 'production' ); if (prodLicenseInfo.clusterAlerts.enabled) { - cluster.alerts = { - list: await fetchStatus( - alertsClient, - req.server.plugins.monitoring.info, - undefined, - cluster.cluster_uuid, - start, - end, - [] - ), - alertsMeta: { - enabled: true, - }, - }; + try { + cluster.alerts = { + list: await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + cluster.cluster_uuid, + start, + end, + [] + ), + alertsMeta: { + enabled: true, + }, + }; + } catch (err) { + req.logger.warn( + `Unable to fetch alert status because '${err.message}'. Alerts may not properly show up in the UI.` + ); + cluster.alerts = { + list: {}, + alertsMeta: { + enabled: true, + }, + }; + } continue; } From 32200af4e93368a020549417e5ff2448275ec12b Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala <bohdan.tsymbala@elastic.co> Date: Wed, 2 Dec 2020 14:47:56 +0100 Subject: [PATCH 054/107] Changed the translation text for the description text in the antivirus registration form (#84626) * Changed the text for the description text in the antivirus registration form. Moved the form component to components folder and extracted translations into constants to make code more readable. * Extracted EventsForm to reduce duplication among events forms. --- .../antivirus_registration_form}/index.tsx | 49 +++-- .../view/components/events_form/index.tsx | 87 ++++++++ .../pages/policy/view/policy_details.tsx | 2 +- .../view/policy_forms/events/checkbox.tsx | 55 ----- .../policy/view/policy_forms/events/linux.tsx | 129 ++++------- .../policy/view/policy_forms/events/mac.tsx | 125 ++++------- .../view/policy_forms/events/translations.ts | 28 --- .../view/policy_forms/events/windows.tsx | 205 +++++++----------- 8 files changed, 281 insertions(+), 399 deletions(-) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/antivirus_registration => components/antivirus_registration_form}/index.tsx (60%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 21fe14df81dd2..072f588663c5a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -14,6 +14,29 @@ import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/se import { usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../../components/config_form'; +const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string }> = { + title: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', + { + defaultMessage: 'Register as anti-virus', + } + ), + description: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: + 'Toggle on to register Elastic as an official Anti-Virus solution for Windows OS. ' + + 'This will also disable Windows Defender.', + } + ), + label: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle', + { + defaultMessage: 'Register as anti-virus', + } + ), +}; + export const AntivirusRegistrationForm = memo(() => { const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); const dispatch = useDispatch(); @@ -30,31 +53,11 @@ export const AntivirusRegistrationForm = memo(() => { ); return ( - <ConfigForm - type={i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', - { - defaultMessage: 'Register as anti-virus', - } - )} - supportedOss={[OperatingSystem.WINDOWS]} - > - <EuiText size="s"> - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', - { - defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', - } - )} - </EuiText> + <ConfigForm type={TRANSLATIONS.title} supportedOss={[OperatingSystem.WINDOWS]}> + <EuiText size="s">{TRANSLATIONS.description}</EuiText> <EuiSpacer size="s" /> <EuiSwitch - label={i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle', - { - defaultMessage: 'Register as anti-virus', - } - )} + label={TRANSLATIONS.label} checked={antivirusRegistrationEnabled} onChange={handleSwitchChange} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx new file mode 100644 index 0000000000000..7cdf54316e4e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCheckbox, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; +import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { OS } from '../../../types'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; + +const OPERATING_SYSTEM_TO_TEST_SUBJ: { [K in OperatingSystem]: string } = { + [OperatingSystem.WINDOWS]: 'Windows', + [OperatingSystem.LINUX]: 'Linux', + [OperatingSystem.MAC]: 'Mac', +}; + +interface OperatingSystemToOsMap { + [OperatingSystem.WINDOWS]: OS.windows; + [OperatingSystem.LINUX]: OS.linux; + [OperatingSystem.MAC]: OS.mac; +} + +export type ProtectionField< + T extends OperatingSystem +> = keyof UIPolicyConfig[OperatingSystemToOsMap[T]]['events']; + +export type EventFormSelection<T extends OperatingSystem> = { [K in ProtectionField<T>]: boolean }; + +export interface EventFormOption<T extends OperatingSystem> { + name: string; + protectionField: ProtectionField<T>; +} + +export interface EventsFormProps<T extends OperatingSystem> { + os: T; + options: ReadonlyArray<EventFormOption<T>>; + selection: EventFormSelection<T>; + onValueSelection: (value: ProtectionField<T>, selected: boolean) => void; +} + +const countSelected = <T extends OperatingSystem>(selection: EventFormSelection<T>) => { + return Object.values(selection).filter((value) => value).length; +}; + +export const EventsForm = <T extends OperatingSystem>({ + os, + options, + selection, + onValueSelection, +}: EventsFormProps<T>) => ( + <ConfigForm + type={i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollection', { + defaultMessage: 'Event Collection', + })} + supportedOss={[os]} + rightCorner={ + <EuiText size="s" color="subdued"> + {i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected: countSelected(selection), total: options.length }, + })} + </EuiText> + } + > + <ConfigFormHeading> + {i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', { + defaultMessage: 'Events', + })} + </ConfigFormHeading> + <EuiSpacer size="s" /> + {options.map(({ name, protectionField }) => ( + <EuiCheckbox + key={String(protectionField)} + id={htmlIdGenerator()()} + label={name} + data-test-subj={`policy${OPERATING_SYSTEM_TO_TEST_SUBJ[os]}Event_${protectionField}`} + checked={selection[protectionField]} + onChange={(event) => onValueSelection(protectionField, event.target.checked)} + /> + ))} + </ConfigForm> +); + +EventsForm.displayName = 'EventsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 9c11bc6f5a4d1..1ce099c494cf0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -36,7 +36,7 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; -import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; +import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx deleted file mode 100644 index 76077831c670b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useMemo } from 'react'; -import { EuiCheckbox, EuiCheckboxProps, htmlIdGenerator } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { PolicyDetailsAction } from '../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; - -type EventsCheckboxProps = Omit<EuiCheckboxProps, 'id' | 'label' | 'checked' | 'onChange'> & { - name: string; - setter: (config: UIPolicyConfig, checked: boolean) => UIPolicyConfig; - getter: (config: UIPolicyConfig) => boolean; -}; - -export const EventsCheckbox = React.memo(function ({ - name, - setter, - getter, - ...otherProps -}: EventsCheckboxProps) { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const selected = getter(policyDetailsConfig); - const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); - const checkboxId = useMemo(() => htmlIdGenerator()(), []); - - const handleCheckboxChange = useCallback( - (event: React.ChangeEvent<HTMLInputElement>) => { - if (policyDetailsConfig) { - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: setter(policyDetailsConfig, event.target.checked) }, - }); - } - }, - [dispatch, policyDetailsConfig, setter] - ); - - return ( - <EuiCheckbox - id={checkboxId} - label={name} - checked={selected} - onChange={handleCheckboxChange} - {...otherProps} - /> - ); -}); - -EventsCheckbox.displayName = 'EventsCheckbox'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 999e3bac5653a..f9532eaecf701 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -4,96 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const LinuxEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedLinuxEvents); - const total = usePolicyDetailsSelector(totalLinuxEvents); - - const checkboxes = useMemo(() => { - const items: Array<{ - name: string; - os: 'linux'; - protectionField: keyof UIPolicyConfig['linux']['events']; - }> = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.linux, - protectionField: 'file', - }, +const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.LINUX>> = [ + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file', { + defaultMessage: 'File', + }), + protectionField: 'file', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.linux, - protectionField: 'process', - }, + defaultMessage: 'Process', + } + ), + protectionField: 'process', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.linux, - protectionField: 'network', - }, - ]; - return ( - <> - <ConfigFormHeading>{EVENTS_HEADING}</ConfigFormHeading> - <EuiSpacer size="s" /> - {items.map((item, index) => { - return ( - <EventsCheckbox - name={item.name} - key={index} - data-test-subj={`policyLinuxEvent_${item.protectionField}`} - setter={(config, checked) => - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - </> - ); - }, []); + defaultMessage: 'Network', + } + ), + protectionField: 'network', + }, +]; + +export const LinuxEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - <ConfigForm - type={EVENTS_FORM_TYPE_LABEL} - supportedOss={[OperatingSystem.LINUX]} - dataTestSubj="linuxEventingForm" - rightCorner={ - <EuiText size="s" color="subdued"> - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - </EuiText> + <EventsForm<OperatingSystem.LINUX> + os={OperatingSystem.LINUX} + selection={policyDetailsConfig.linux.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setIn(policyDetailsConfig)('linux')('events')(value)(selected) }, + }) } - > - {checkboxes} - </ConfigForm> + /> ); }); + +LinuxEvents.displayName = 'LinuxEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index 6e15a3c4cd43b..ac6ae531ba172 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -4,96 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const MacEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedMacEvents); - const total = usePolicyDetailsSelector(totalMacEvents); +const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.MAC>> = [ + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file', { + defaultMessage: 'File', + }), + protectionField: 'file', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.process', { + defaultMessage: 'Process', + }), + protectionField: 'process', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network', { + defaultMessage: 'Network', + }), + protectionField: 'network', + }, +]; - const checkboxes = useMemo(() => { - const items: Array<{ - name: string; - os: 'mac'; - protectionField: keyof UIPolicyConfig['mac']['events']; - }> = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.mac, - protectionField: 'file', - }, - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.mac, - protectionField: 'process', - }, - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.mac, - protectionField: 'network', - }, - ]; - return ( - <> - <ConfigFormHeading>{EVENTS_HEADING}</ConfigFormHeading> - <EuiSpacer size="s" /> - {items.map((item, index) => { - return ( - <EventsCheckbox - name={item.name} - key={index} - data-test-subj={`policyMacEvent_${item.protectionField}`} - setter={(config, checked) => - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - </> - ); - }, []); +export const MacEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - <ConfigForm - type={EVENTS_FORM_TYPE_LABEL} - supportedOss={[OperatingSystem.MAC]} - dataTestSubj="macEventingForm" - rightCorner={ - <EuiText size="s" color="subdued"> - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - </EuiText> + <EventsForm<OperatingSystem.MAC> + os={OperatingSystem.MAC} + selection={policyDetailsConfig.mac.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setIn(policyDetailsConfig)('mac')('events')(value)(selected) }, + }) } - > - {checkboxes} - </ConfigForm> + /> ); }); + +MacEvents.displayName = 'MacEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts deleted file mode 100644 index 3b48b7969a8ce..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const EVENTS_HEADING = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', - { - defaultMessage: 'Events', - } -); - -export const EVENTS_FORM_TYPE_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.eventCollection', - { - defaultMessage: 'Event Collection', - } -); - -export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { - return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { - defaultMessage: '{selected} / {total} event collections enabled', - values: { selected, total }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index c381249cf24b9..c99f2a6b72ac3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -4,142 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { - Immutable, - OperatingSystem, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const WindowsEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedWindowsEvents); - const total = usePolicyDetailsSelector(totalWindowsEvents); - - const checkboxes = useMemo(() => { - const items: Immutable< - Array<{ - name: string; - os: 'windows'; - protectionField: keyof UIPolicyConfig['windows']['events']; - }> - > = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', - { - defaultMessage: 'DLL and Driver Load', - } - ), - os: OS.windows, - protectionField: 'dll_and_driver_load', - }, +const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.WINDOWS>> = [ + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dns', - { - defaultMessage: 'DNS', - } - ), - os: OS.windows, - protectionField: 'dns', - }, + defaultMessage: 'DLL and Driver Load', + } + ), + protectionField: 'dll_and_driver_load', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dns', { + defaultMessage: 'DNS', + }), + protectionField: 'dns', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.file', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.windows, - protectionField: 'file', - }, + defaultMessage: 'File', + } + ), + protectionField: 'file', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.network', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.windows, - protectionField: 'network', - }, + defaultMessage: 'Network', + } + ), + protectionField: 'network', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.process', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.windows, - protectionField: 'process', - }, + defaultMessage: 'Process', + } + ), + protectionField: 'process', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.registry', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.registry', - { - defaultMessage: 'Registry', - } - ), - os: OS.windows, - protectionField: 'registry', - }, + defaultMessage: 'Registry', + } + ), + protectionField: 'registry', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.security', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.security', - { - defaultMessage: 'Security', - } - ), - os: OS.windows, - protectionField: 'security', - }, - ]; - return ( - <> - <ConfigFormHeading>{EVENTS_HEADING}</ConfigFormHeading> - <EuiSpacer size="s" /> - {items.map((item, index) => { - return ( - <EventsCheckbox - name={item.name} - key={index} - data-test-subj={`policyWindowsEvent_${item.protectionField}`} - setter={(config, checked) => - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - </> - ); - }, []); + defaultMessage: 'Security', + } + ), + protectionField: 'security', + }, +]; + +export const WindowsEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - <ConfigForm - type={EVENTS_FORM_TYPE_LABEL} - supportedOss={[OperatingSystem.WINDOWS]} - dataTestSubj="windowsEventingForm" - rightCorner={ - <EuiText size="s" color="subdued"> - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - </EuiText> + <EventsForm<OperatingSystem.WINDOWS> + os={OperatingSystem.WINDOWS} + selection={policyDetailsConfig.windows.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { + policyConfig: setIn(policyDetailsConfig)('windows')('events')(value)(selected), + }, + }) } - > - {checkboxes} - </ConfigForm> + /> ); }); + +WindowsEvents.displayName = 'WindowsEvents'; From c1e7f69ca1bf3e9abe787fb1d2dfa5847a6ec73c Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin <aleh.zasypkin@gmail.com> Date: Wed, 2 Dec 2020 15:00:01 +0100 Subject: [PATCH 055/107] Migrate security routes to a new Elasticsearch client. (#84528) --- .../elasticsearch_client_plugin.ts | 254 ------------------ x-pack/plugins/security/server/plugin.ts | 1 - .../server/routes/api_keys/enabled.test.ts | 165 +++++------- .../server/routes/api_keys/get.test.ts | 27 +- .../security/server/routes/api_keys/get.ts | 10 +- .../server/routes/api_keys/invalidate.test.ts | 41 ++- .../server/routes/api_keys/invalidate.ts | 8 +- .../server/routes/api_keys/privileges.test.ts | 233 +++++----------- .../server/routes/api_keys/privileges.ts | 26 +- .../authorization/privileges/get_builtin.ts | 8 +- .../routes/authorization/roles/delete.test.ts | 30 +-- .../routes/authorization/roles/delete.ts | 4 +- .../routes/authorization/roles/get.test.ts | 30 +-- .../server/routes/authorization/roles/get.ts | 12 +- .../authorization/roles/get_all.test.ts | 27 +- .../routes/authorization/roles/get_all.ts | 10 +- .../routes/authorization/roles/put.test.ts | 182 ++++++------- .../server/routes/authorization/roles/put.ts | 21 +- .../security/server/routes/index.mock.ts | 2 - .../plugins/security/server/routes/index.ts | 9 +- .../server/routes/indices/get_fields.test.ts | 14 +- .../server/routes/indices/get_fields.ts | 16 +- .../server/routes/role_mapping/delete.test.ts | 52 ++-- .../server/routes/role_mapping/delete.ts | 14 +- .../routes/role_mapping/feature_check.test.ts | 92 +++---- .../routes/role_mapping/feature_check.ts | 46 ++-- .../server/routes/role_mapping/get.test.ts | 100 +++---- .../server/routes/role_mapping/get.ts | 12 +- .../server/routes/role_mapping/post.test.ts | 71 +++-- .../server/routes/role_mapping/post.ts | 15 +- .../routes/users/change_password.test.ts | 98 +++---- .../server/routes/users/change_password.ts | 45 ++-- .../server/routes/users/create_or_update.ts | 4 +- .../security/server/routes/users/delete.ts | 8 +- .../security/server/routes/users/get.ts | 12 +- .../security/server/routes/users/get_all.ts | 4 +- 36 files changed, 610 insertions(+), 1093 deletions(-) diff --git a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts index 529e8a8aa6e9c..0a43d8dd6973a 100644 --- a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts +++ b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts @@ -22,133 +22,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen }, }); - /** - * Perform a [shield.changePassword](Change the password of a user) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the user to change the password for - */ - shield.changePassword = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - urls: [ - { - fmt: '/_security/user/<%=username%>/_password', - req: { - username: { - type: 'string', - required: false, - }, - }, - }, - { - fmt: '/_security/user/_password', - }, - ], - needBody: true, - method: 'POST', - }); - - /** - * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache - * @param {String} params.realms - Comma-separated list of realms to clear - */ - shield.clearCachedRealms = ca({ - params: { - usernames: { - type: 'string', - required: false, - }, - }, - url: { - fmt: '/_security/realm/<%=realms%>/_clear_cache', - req: { - realms: { - type: 'string', - required: true, - }, - }, - }, - method: 'POST', - }); - - /** - * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.clearCachedRoles = ca({ - params: {}, - url: { - fmt: '/_security/role/<%=name%>/_clear_cache', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - method: 'POST', - }); - - /** - * Perform a [shield.deleteRole](Remove a role from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.deleteRole = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - method: 'DELETE', - }); - - /** - * Perform a [shield.deleteUser](Remove a user from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - username - */ - shield.deleteUser = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true, - }, - }, - }, - method: 'DELETE', - }); - /** * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request * @@ -173,30 +46,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen ], }); - /** - * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String, String[], Boolean} params.username - A comma-separated list of usernames - */ - shield.getUser = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'list', - required: false, - }, - }, - }, - { - fmt: '/_security/user', - }, - ], - }); - /** * Perform a [shield.putRole](Update or create a role for the native shield realm) request * @@ -249,19 +98,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen method: 'PUT', }); - /** - * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request - * - */ - shield.getUserPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/_privileges', - }, - ], - }); - /** * Asks Elasticsearch to prepare SAML authentication request to be sent to * the 3rd-party SAML identity provider. @@ -489,36 +325,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen }, }); - shield.getBuiltinPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/privilege/_builtin', - }, - ], - }); - - /** - * Gets API keys in Elasticsearch - * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. - * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as - * they are assumed to be the currently authenticated ones. - */ - shield.getAPIKeys = ca({ - method: 'GET', - urls: [ - { - fmt: `/_security/api_key?owner=<%=owner%>`, - req: { - owner: { - type: 'boolean', - required: true, - }, - }, - }, - ], - }); - /** * Creates an API key in Elasticsearch for the current user. * @@ -591,64 +397,4 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen fmt: '/_security/delegate_pki', }, }); - - /** - * Retrieves all configured role mappings. - * - * @returns {{ [roleMappingName]: { enabled: boolean; roles: string[]; rules: Record<string, any>} }} - */ - shield.getRoleMappings = ca({ - method: 'GET', - urls: [ - { - fmt: '/_security/role_mapping', - }, - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); - - /** - * Saves the specified role mapping. - */ - shield.saveRoleMapping = ca({ - method: 'POST', - needBody: true, - urls: [ - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); - - /** - * Deletes the specified role mapping. - */ - shield.deleteRoleMapping = ca({ - method: 'DELETE', - urls: [ - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 17f2480026cc7..d6fe1356ce145 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -230,7 +230,6 @@ export class Plugin { basePath: core.http.basePath, httpResources: core.http.resources, logger: this.initializerContext.logger.get('routes'), - clusterClient, config, authc: this.authc, authz, diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index 7968402cd2176..3a22b9fe003a1 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -4,115 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; -import { LicenseCheck } from '../../../../licensing/server'; +import Boom from '@hapi/boom'; +import { + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from '../../../../../../src/core/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; -import Boom from '@hapi/boom'; -import { defineEnabledApiKeysRoutes } from './enabled'; -import { APIKeys } from '../../authentication/api_keys'; -interface TestOptions { - licenseCheckResult?: LicenseCheck; - apiResponse?: () => Promise<unknown>; - asserts: { statusCode: number; result?: Record<string, any> }; -} +import { defineEnabledApiKeysRoutes } from './enabled'; +import { Authentication } from '../../authentication'; describe('API keys enabled', () => { - const enabledApiKeysTest = ( - description: string, - { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions - ) => { - test(description, async () => { - const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const apiKeys = new APIKeys({ - logger: mockRouteDefinitionParams.logger, - clusterClient: mockRouteDefinitionParams.clusterClient, - license: mockRouteDefinitionParams.license, - }); - - mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => - apiKeys.areAPIKeysEnabled() + function getMockContext( + licenseCheckResult: { state: string; message?: string } = { state: 'valid' } + ) { + return ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + } + + let routeHandler: RequestHandler<any, any, any, any>; + let authc: jest.Mocked<Authentication>; + beforeEach(() => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + authc = mockRouteDefinitionParams.authc; + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + + const [, apiKeyRouteHandler] = mockRouteDefinitionParams.router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/api_key/_enabled' + )!; + routeHandler = apiKeyRouteHandler; + }); + + describe('failure', () => { + test('returns result of license checker', async () => { + const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); + const response = await routeHandler( + mockContext, + httpServerMock.createKibanaRequest(), + kibanaResponseFactory ); - if (apiResponse) { - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); - } - - defineEnabledApiKeysRoutes(mockRouteDefinitionParams); - const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; - - const headers = { authorization: 'foo' }; - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'get', - path: '/internal/security/api_key/_enabled', - headers, - }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; - - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); - - if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.invalidateAPIKey', - { - body: { - id: expect.any(String), - }, - } - ); - } else { - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); - } + expect(response.status).toBe(403); + expect(response.payload).toEqual({ message: 'test forbidden message' }); expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); - }; - describe('failure', () => { - enabledApiKeysTest('returns result of license checker', { - licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, - asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, - }); + test('returns error from cluster client', async () => { + const error = Boom.notAcceptable('test not acceptable message'); + authc.areAPIKeysEnabled.mockRejectedValue(error); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); - const error = Boom.notAcceptable('test not acceptable message'); - enabledApiKeysTest('returns error from cluster client', { - apiResponse: async () => { - throw error; - }, - asserts: { statusCode: 406, result: error }, + expect(response.status).toBe(406); + expect(response.payload).toEqual(error); }); }); describe('success', () => { - enabledApiKeysTest('returns true if API Keys are enabled', { - apiResponse: async () => ({}), - asserts: { - statusCode: 200, - result: { - apiKeysEnabled: true, - }, - }, + test('returns true if API Keys are enabled', async () => { + authc.areAPIKeysEnabled.mockResolvedValue(true); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ apiKeysEnabled: true }); }); - enabledApiKeysTest('returns false if API Keys are disabled', { - apiResponse: async () => { - const error = new Error(); - (error as any).body = { - error: { 'disabled.feature': 'api_keys' }, - }; - throw error; - }, - asserts: { - statusCode: 200, - result: { - apiKeysEnabled: false, - }, - }, + + test('returns false if API Keys are disabled', async () => { + authc.areAPIKeysEnabled.mockResolvedValue(false); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ apiKeysEnabled: false }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.test.ts b/x-pack/plugins/security/server/routes/api_keys/get.test.ts index cb991fb2f5aac..cc9e9a68e6e36 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineGetApiKeysRoutes } from './get'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, coreMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import Boom from '@hapi/boom'; @@ -26,11 +26,15 @@ describe('Get API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getApiKey.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetApiKeysRoutes(mockRouteDefinitionParams); @@ -43,22 +47,15 @@ describe('Get API keys', () => { query: { isAdmin: isAdmin.toString() }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getAPIKeys', - { owner: !isAdmin } - ); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getApiKey + ).toHaveBeenCalledWith({ owner: !isAdmin }); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.ts b/x-pack/plugins/security/server/routes/api_keys/get.ts index 6e98b4b098405..b0c6ec090a0e5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.ts @@ -10,7 +10,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetApiKeysRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key', @@ -28,11 +28,11 @@ export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitio createLicensedRouteHandler(async (context, request, response) => { try { const isAdmin = request.query.isAdmin === 'true'; - const { api_keys: apiKeys } = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getAPIKeys', { owner: !isAdmin })) as { api_keys: ApiKey[] }; + const apiResponse = await context.core.elasticsearch.client.asCurrentUser.security.getApiKey<{ + api_keys: ApiKey[]; + }>({ owner: !isAdmin }); - const validKeys = apiKeys.filter(({ invalidated }) => !invalidated); + const validKeys = apiResponse.body.api_keys.filter(({ invalidated }) => !invalidated); return response.ok({ body: { apiKeys: validKeys } }); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts index 88e52f735395d..9ac41fdfa7483 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts @@ -6,18 +6,18 @@ import Boom from '@hapi/boom'; import { Type } from '@kbn/config-schema'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineInvalidateApiKeysRoutes } from './invalidate'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; interface TestOptions { licenseCheckResult?: LicenseCheck; apiResponses?: Array<() => Promise<unknown>>; payload?: Record<string, any>; - asserts: { statusCode: number; result?: Record<string, any>; apiArguments?: unknown[][] }; + asserts: { statusCode: number; result?: Record<string, any>; apiArguments?: unknown[] }; } describe('Invalidate API keys', () => { @@ -27,10 +27,15 @@ describe('Invalidate API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; + for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey.mockImplementationOnce( + (async () => ({ body: await apiResponse() })) as any + ); } defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); @@ -43,9 +48,6 @@ describe('Invalidate API keys', () => { body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); @@ -53,13 +55,10 @@ describe('Invalidate API keys', () => { if (Array.isArray(asserts.apiArguments)) { for (const apiArguments of asserts.apiArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( - mockRequest - ); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey + ).toHaveBeenCalledWith(apiArguments); } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); @@ -128,7 +127,7 @@ describe('Invalidate API keys', () => { isAdmin: true, }, asserts: { - apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV' } }], statusCode: 200, result: { itemsInvalidated: [], @@ -152,7 +151,7 @@ describe('Invalidate API keys', () => { isAdmin: true, }, asserts: { - apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV' } }], statusCode: 200, result: { itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], @@ -168,9 +167,7 @@ describe('Invalidate API keys', () => { isAdmin: false, }, asserts: { - apiArguments: [ - ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], - ], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], statusCode: 200, result: { itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], @@ -195,8 +192,8 @@ describe('Invalidate API keys', () => { }, asserts: { apiArguments: [ - ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }], - ['shield.invalidateAPIKey', { body: { id: 'ab8If24B1bKsmSLTAhNC' } }], + { body: { id: 'si8If24B1bKsmSLTAhJV' } }, + { body: { id: 'ab8If24B1bKsmSLTAhNC' } }, ], statusCode: 200, result: { diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts index dd472c0b60cbc..3977954197007 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -15,7 +15,7 @@ interface ResponseType { errors: Array<Pick<ApiKey, 'id' | 'name'> & { error: Error }>; } -export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineInvalidateApiKeysRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/api_key/invalidate', @@ -28,8 +28,6 @@ export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDe }, createLicensedRouteHandler(async (context, request, response) => { try { - const scopedClusterClient = clusterClient.asScoped(request); - // Invalidate all API keys in parallel. const invalidationResult = ( await Promise.all( @@ -41,7 +39,9 @@ export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDe } // Send the request to invalidate the API key and return an error if it could not be deleted. - await scopedClusterClient.callAsCurrentUser('shield.invalidateAPIKey', { body }); + await context.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey({ + body, + }); return { key, error: undefined }; } catch (error) { return { key, error: wrapError(error) }; diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index ecc3d32e20aec..b06d1329dc1db 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -6,23 +6,17 @@ import Boom from '@hapi/boom'; import { LicenseCheck } from '../../../../licensing/server'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; -import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - callAsInternalUserResponses?: Array<() => Promise<unknown>>; - callAsCurrentUserResponses?: Array<() => Promise<unknown>>; - asserts: { - statusCode: number; - result?: Record<string, any>; - callAsInternalUserAPIArguments?: unknown[][]; - callAsCurrentUserAPIArguments?: unknown[][]; - }; + areAPIKeysEnabled?: boolean; + apiResponse?: () => Promise<unknown>; + asserts: { statusCode: number; result?: Record<string, any>; apiArguments?: unknown }; } describe('Check API keys privileges', () => { @@ -30,32 +24,23 @@ describe('Check API keys privileges', () => { description: string, { licenseCheckResult = { state: 'valid' }, - callAsInternalUserResponses = [], - callAsCurrentUserResponses = [], + areAPIKeysEnabled = true, + apiResponse, asserts, }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const apiKeys = new APIKeys({ - logger: mockRouteDefinitionParams.logger, - clusterClient: mockRouteDefinitionParams.clusterClient, - license: mockRouteDefinitionParams.license, - }); - - mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => - apiKeys.areAPIKeysEnabled() - ); + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockResolvedValue(areAPIKeysEnabled); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of callAsCurrentUserResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } - for (const apiResponse of callAsInternalUserResponses) { - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( - apiResponse + if (apiResponse) { + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockImplementation( + (async () => ({ body: await apiResponse() })) as any ); } @@ -68,33 +53,15 @@ describe('Check API keys privileges', () => { path: '/internal/security/api_key/privileges', headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { - for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( - mockRequest - ); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - - if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { - for (const apiArguments of asserts.callAsInternalUserAPIArguments) { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( - ...apiArguments - ); - } - } else { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + if (asserts.apiArguments) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges + ).toHaveBeenCalledWith(asserts.apiArguments); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); @@ -109,22 +76,13 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - callAsCurrentUserResponses: [ - async () => { - throw error; - }, - ], - callAsInternalUserResponses: [async () => {}], + apiResponse: async () => { + throw error; + }, asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 406, result: error, }, @@ -133,40 +91,17 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [ - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, - ], - }), - ], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, @@ -175,36 +110,18 @@ describe('Check API keys privileges', () => { getPrivilegesTest( 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [ - async () => { - const error = new Error(); - (error as any).body = { - error: { - 'disabled.feature': 'api_keys', - }, - }; - throw error; - }, - ], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, + index: {}, + application: {}, + }), + areAPIKeysEnabled: false, asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, @@ -212,52 +129,34 @@ describe('Check API keys privileges', () => { ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [async () => ({})], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, }, }); getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [async () => ({})], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 9cccb96752772..dd5d81060c7e5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,11 +8,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ - router, - clusterClient, - authc, -}: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ router, authc }: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,19 +16,25 @@ export function defineCheckPrivilegesRoutes({ }, createLicensedRouteHandler(async (context, request, response) => { try { - const scopedClusterClient = clusterClient.asScoped(request); - const [ { - cluster: { - manage_security: manageSecurity, - manage_api_key: manageApiKey, - manage_own_api_key: manageOwnApiKey, + body: { + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, }, areApiKeysEnabled, ] = await Promise.all([ - scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<{ + cluster: { + manage_security: boolean; + manage_api_key: boolean; + manage_own_api_key: boolean; + }; + }>({ body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), authc.areAPIKeysEnabled(), diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index 08cd3ba487b0b..39e6a4838d34d 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -7,13 +7,13 @@ import { BuiltinESPrivileges } from '../../../../common/model'; import { RouteDefinitionParams } from '../..'; -export function defineGetBuiltinPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/esPrivileges/builtin', validate: false }, async (context, request, response) => { - const privileges: BuiltinESPrivileges = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getBuiltinPrivileges'); + const { + body: privileges, + } = await context.core.elasticsearch.client.asCurrentUser.security.getBuiltinPrivileges<BuiltinESPrivileges>(); // Exclude the `none` privilege, as it doesn't make sense as an option within the Kibana UI privileges.cluster = privileges.cluster.filter((privilege) => privilege !== 'none'); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index 9f5ec635f56cd..5143b727fcb4e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -5,14 +5,11 @@ */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineDeleteRolesRoutes } from './delete'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; interface TestOptions { @@ -29,11 +26,15 @@ describe('DELETE role', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineDeleteRolesRoutes(mockRouteDefinitionParams); @@ -46,22 +47,15 @@ describe('DELETE role', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.deleteRole', - { name } - ); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRole + ).toHaveBeenCalledWith({ name }); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index eb56143288747..b877aaf6abd77 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -9,7 +9,7 @@ import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; -export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineDeleteRolesRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/api/security/role/{name}', @@ -19,7 +19,7 @@ export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefiniti }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient.asScoped(request).callAsCurrentUser('shield.deleteRole', { + await context.core.elasticsearch.client.asCurrentUser.security.deleteRole({ name: request.params.name, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index b25b13b9fc04a..a6090ee405329 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineGetRolesRoutes } from './get'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; const application = 'kibana-.kibana'; @@ -32,11 +29,15 @@ describe('GET role', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetRolesRoutes(mockRouteDefinitionParams); @@ -49,22 +50,17 @@ describe('GET role', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole', { - name, - }); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalledWith({ name }); } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index bf1140e2e6fd1..ce4a622d30e61 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -8,9 +8,9 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; -import { transformElasticsearchRoleToRole } from './model'; +import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; -export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { router.get( { path: '/api/security/role/{name}', @@ -20,9 +20,11 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi }, createLicensedRouteHandler(async (context, request, response) => { try { - const elasticsearchRoles = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole', { name: request.params.name }); + const { + body: elasticsearchRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record<string, ElasticsearchRole> + >({ name: request.params.name }); const elasticsearchRole = elasticsearchRoles[request.params.name]; if (elasticsearchRole) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 30e0c52c4c443..b3a855b2e0ae7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineGetAllRolesRoutes } from './get_all'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; const application = 'kibana-.kibana'; @@ -32,11 +29,15 @@ describe('GET all roles', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetAllRolesRoutes(mockRouteDefinitionParams); @@ -48,19 +49,15 @@ describe('GET all roles', () => { path: '/api/security/role', headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole'); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 24be6c60e4b12..21521dd6dbae3 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -9,14 +9,16 @@ import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; -export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams) { router.get( { path: '/api/security/role', validate: false }, createLicensedRouteHandler(async (context, request, response) => { try { - const elasticsearchRoles = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole')) as Record<string, ElasticsearchRole>; + const { + body: elasticsearchRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record<string, ElasticsearchRole> + >(); // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. return response.ok({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 811ea080b4316..779e1a7fab177 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -5,15 +5,12 @@ */ import { Type } from '@kbn/config-schema'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { GLOBAL_RESOURCE } from '../../../../common/constants'; import { definePutRolesRoutes } from './put'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; import { KibanaFeature } from '../../../../../features/server'; import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; @@ -47,35 +44,43 @@ const privilegeMap = { interface TestOptions { name: string; licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise<unknown>>; + apiResponses?: { + get: () => Promise<unknown>; + put: () => Promise<unknown>; + }; payload?: Record<string, any>; asserts: { statusCode: number; result?: Record<string, any>; - apiArguments?: unknown[][]; + apiArguments?: { get: unknown[]; put: unknown[] }; recordSubFeaturePrivilegeUsage?: boolean; }; } const putRoleTest = ( description: string, - { - name, - payload, - licenseCheckResult = { state: 'valid' }, - apiResponses = [], - asserts, - }: TestOptions + { name, payload, licenseCheckResult = { state: 'valid' }, apiResponses, asserts }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; + + if (apiResponses?.get) { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementationOnce( + (async () => ({ body: await apiResponses?.get() })) as any + ); + } + + if (apiResponses?.put) { + mockContext.core.elasticsearch.client.asCurrentUser.security.putRole.mockImplementationOnce( + (async () => ({ body: await apiResponses?.put() })) as any + ); } mockRouteDefinitionParams.getFeatureUsageService.mockReturnValue( @@ -131,21 +136,20 @@ const putRoleTest = ( body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + if (asserts.apiArguments?.get) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalledWith(...asserts.apiArguments?.get); + } + if (asserts.apiArguments?.put) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRole + ).toHaveBeenCalledWith(...asserts.apiArguments?.put); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); @@ -208,12 +212,11 @@ describe('PUT role', () => { putRoleTest(`creates empty role`, { name: 'foo-role', payload: {}, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -224,7 +227,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -239,12 +242,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -261,7 +263,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -279,12 +281,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -301,7 +302,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -317,12 +318,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -339,7 +339,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -383,12 +383,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -426,7 +425,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -473,8 +472,8 @@ describe('PUT role', () => { }, ], }, - apiResponses: [ - async () => ({ + apiResponses: { + get: async () => ({ 'foo-role': { metadata: { bar: 'old-metadata', @@ -504,13 +503,12 @@ describe('PUT role', () => { ], }, }), - async () => {}, - ], + put: async () => {}, + }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -548,7 +546,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -577,8 +575,8 @@ describe('PUT role', () => { }, ], }, - apiResponses: [ - async () => ({ + apiResponses: { + get: async () => ({ 'foo-role': { metadata: { bar: 'old-metadata', @@ -613,13 +611,12 @@ describe('PUT role', () => { ], }, }), - async () => {}, - ], + put: async () => {}, + }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -652,7 +649,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -670,13 +667,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: true, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -694,7 +690,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -712,13 +708,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: false, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -736,7 +731,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -754,13 +749,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: false, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -778,7 +772,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index cdedc9ac8a5eb..26c61b4ced15f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -42,7 +42,6 @@ const roleGrantsSubFeaturePrivileges = ( export function definePutRolesRoutes({ router, authz, - clusterClient, getFeatures, getFeatureUsageService, }: RouteDefinitionParams) { @@ -64,12 +63,11 @@ export function definePutRolesRoutes({ const { name } = request.params; try { - const rawRoles: Record<string, ElasticsearchRole> = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole', { - name: request.params.name, - ignore: [404], - }); + const { + body: rawRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record<string, ElasticsearchRole> + >({ name: request.params.name }, { ignore: [404] }); const body = transformPutPayloadToElasticsearchRole( request.body, @@ -77,11 +75,12 @@ export function definePutRolesRoutes({ rawRoles[name] ? rawRoles[name].applications : [] ); - const [features] = await Promise.all<KibanaFeature[]>([ + const [features] = await Promise.all([ getFeatures(), - clusterClient - .asScoped(request) - .callAsCurrentUser('shield.putRole', { name: request.params.name, body }), + context.core.elasticsearch.client.asCurrentUser.security.putRole({ + name: request.params.name, + body, + }), ]); if (roleGrantsSubFeaturePrivileges(features, request.body)) { diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index fab4a71df0cb0..1df499d981632 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -5,7 +5,6 @@ */ import { - elasticsearchServiceMock, httpServiceMock, loggingSystemMock, httpResourcesMock, @@ -25,7 +24,6 @@ export const routeDefinitionParamsMock = { basePath: httpServiceMock.createBasePath(), csp: httpServiceMock.createSetupContract().csp, logger: loggingSystemMock.create().get(), - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), { isTLSEnabled: false, }), diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 079c9e8ab9ce7..db71b04b3e6f0 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -5,13 +5,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaFeature } from '../../../features/server'; -import { - HttpResources, - IBasePath, - ILegacyClusterClient, - IRouter, - Logger, -} from '../../../../../src/core/server'; +import { HttpResources, IBasePath, IRouter, Logger } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; import { AuthorizationServiceSetup } from '../authorization'; @@ -36,7 +30,6 @@ export interface RouteDefinitionParams { basePath: IBasePath; httpResources: HttpResources; logger: Logger; - clusterClient: ILegacyClusterClient; config: ConfigType; authc: Authentication; authz: AuthorizationServiceSetup; diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.test.ts b/x-pack/plugins/security/server/routes/indices/get_fields.test.ts index 4c6182e99431d..6d3de11249a16 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.test.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock, elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, coreMock } from '../../../../../../src/core/server/mocks'; import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -36,10 +36,12 @@ const mockFieldMappingResponse = { describe('GET /internal/security/fields/{query}', () => { it('returns a list of deduplicated fields, omitting empty and runtime fields', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const scopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - scopedClient.callAsCurrentUser.mockResolvedValue(mockFieldMappingResponse); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(scopedClient); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + }; + mockContext.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping.mockImplementation( + (async () => ({ body: mockFieldMappingResponse })) as any + ); defineGetFieldsRoutes(mockRouteDefinitionParams); @@ -51,7 +53,7 @@ describe('GET /internal/security/fields/{query}', () => { path: `/internal/security/fields/foo`, headers, }); - const response = await handler({} as any, mockRequest, kibanaResponseFactory); + const response = await handler(mockContext as any, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual(['fooField', 'commonField', 'barField']); }); diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index 44b8804ed8d6e..304e121f7fee1 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -22,7 +22,7 @@ interface FieldMappingResponse { }; } -export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/fields/{query}', @@ -30,14 +30,16 @@ export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinition }, async (context, request, response) => { try { - const indexMappings = (await clusterClient - .asScoped(request) - .callAsCurrentUser('indices.getFieldMapping', { + const { + body: indexMappings, + } = await context.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping<FieldMappingResponse>( + { index: request.params.query, fields: '*', - allowNoIndices: false, - includeDefaults: true, - })) as FieldMappingResponse; + allow_no_indices: false, + include_defaults: true, + } + ); // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): // 1. Iterate over all matched indices. diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts index aec0310129f6e..33fd66f9e929d 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts @@ -5,24 +5,26 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { defineRoleMappingDeleteRoutes } from './delete'; describe('DELETE role mappings', () => { it('allows a role mapping to be deleted', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ acknowledged: true }); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } } as any, + }; + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping.mockResolvedValue( + { body: { acknowledged: true } } as any + ); defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; const name = 'mapping1'; - const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'delete', @@ -30,31 +32,35 @@ describe('DELETE role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual({ acknowledged: true }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); expect( - mockScopedClusterClient.callAsCurrentUser - ).toHaveBeenCalledWith('shield.deleteRoleMapping', { name }); + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping + ).toHaveBeenCalledWith({ name }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: 'invalid', + message: 'test forbidden message', + }), + }, + } as any, + }; defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; const name = 'mapping1'; - const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'delete', @@ -62,21 +68,13 @@ describe('DELETE role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping + ).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.ts index dc11bcd914b35..dbe9c5662a1f1 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.ts @@ -8,9 +8,7 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; -export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { - const { clusterClient, router } = params; - +export function defineRoleMappingDeleteRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/role_mapping/{name}', @@ -22,11 +20,11 @@ export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const deleteResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.deleteRoleMapping', { - name: request.params.name, - }); + const { + body: deleteResponse, + } = await context.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping({ + name: request.params.name, + }); return response.ok({ body: deleteResponse }); } catch (error) { const wrappedError = wrapError(error); diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts index ee1d550bbe24d..8bd9f095b0f68 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts @@ -5,21 +5,16 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { - kibanaResponseFactory, - RequestHandlerContext, - ILegacyClusterClient, -} from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineRoleMappingFeatureCheckRoute } from './feature_check'; interface TestOptions { licenseCheckResult?: LicenseCheck; canManageRoleMappings?: boolean; - nodeSettingsResponse?: Record<string, any>; - xpackUsageResponse?: Record<string, any>; - internalUserClusterClientImpl?: ILegacyClusterClient['callAsInternalUser']; + nodeSettingsResponse?: () => Record<string, any>; + xpackUsageResponse?: () => Record<string, any>; asserts: { statusCode: number; result?: Record<string, any> }; } @@ -38,57 +33,34 @@ const defaultXpackUsageResponse = { }, }; -const getDefaultInternalUserClusterClientImpl = ( - nodeSettingsResponse: TestOptions['nodeSettingsResponse'], - xpackUsageResponse: TestOptions['xpackUsageResponse'] -) => - ((async (endpoint: string, clientParams: Record<string, any>) => { - if (!clientParams) throw new TypeError('expected clientParams'); - - if (endpoint === 'nodes.info') { - return nodeSettingsResponse; - } - - if (endpoint === 'transport.request') { - if (clientParams.path === '/_xpack/usage') { - return xpackUsageResponse; - } - } - - throw new Error(`unexpected endpoint: ${endpoint}`); - }) as unknown) as TestOptions['internalUserClusterClientImpl']; - describe('GET role mappings feature check', () => { const getFeatureCheckTest = ( description: string, { licenseCheckResult = { state: 'valid' }, canManageRoleMappings = true, - nodeSettingsResponse = {}, - xpackUsageResponse = defaultXpackUsageResponse, - internalUserClusterClientImpl = getDefaultInternalUserClusterClientImpl( - nodeSettingsResponse, - xpackUsageResponse - ), + nodeSettingsResponse = async () => ({}), + xpackUsageResponse = async () => defaultXpackUsageResponse, asserts, }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation( - internalUserClusterClientImpl + mockContext.core.elasticsearch.client.asInternalUser.nodes.info.mockImplementation( + (async () => ({ body: await nodeSettingsResponse() })) as any + ); + mockContext.core.elasticsearch.client.asInternalUser.transport.request.mockImplementation( + (async () => ({ body: await xpackUsageResponse() })) as any ); - mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method, payload) => { - if (method === 'shield.hasPrivileges') { - return { - has_all_requested: canManageRoleMappings, - }; - } - }); + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue({ + body: { has_all_requested: canManageRoleMappings }, + } as any); defineRoleMappingFeatureCheckRoute(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -99,9 +71,6 @@ describe('GET role mappings feature check', () => { path: `/internal/security/_check_role_mapping_features`, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); @@ -124,7 +93,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('allows both script types when explicitly enabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -134,7 +103,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -147,7 +116,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('disallows stored scripts when disabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -157,7 +126,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -170,7 +139,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('disallows inline scripts when disabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -180,7 +149,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -193,7 +162,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('indicates incompatible realms when only native and file are enabled', { - xpackUsageResponse: { + xpackUsageResponse: async () => ({ security: { realms: { native: { @@ -206,7 +175,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -231,9 +200,12 @@ describe('GET role mappings feature check', () => { getFeatureCheckTest( 'falls back to allowing both script types if there is an error retrieving node settings', { - internalUserClusterClientImpl: (() => { - return Promise.reject(new Error('something bad happened')); - }) as TestOptions['internalUserClusterClientImpl'], + nodeSettingsResponse: async () => { + throw new Error('something bad happened'); + }, + xpackUsageResponse: async () => { + throw new Error('something bad happened'); + }, asserts: { statusCode: 200, result: { diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts index 88c7f193cea34..470039b8ae92b 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, ILegacyClusterClient } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; @@ -34,24 +34,18 @@ interface XPackUsageResponse { const INCOMPATIBLE_REALMS = ['file', 'native']; -export function defineRoleMappingFeatureCheckRoute({ - router, - clusterClient, - logger, -}: RouteDefinitionParams) { +export function defineRoleMappingFeatureCheckRoute({ router, logger }: RouteDefinitionParams) { router.get( { path: '/internal/security/_check_role_mapping_features', validate: false, }, createLicensedRouteHandler(async (context, request, response) => { - const { has_all_requested: canManageRoleMappings } = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.hasPrivileges', { - body: { - cluster: ['manage_security'], - }, - }); + const { + body: { has_all_requested: canManageRoleMappings }, + } = await context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<{ + has_all_requested: boolean; + }>({ body: { cluster: ['manage_security'] } }); if (!canManageRoleMappings) { return response.ok({ @@ -61,7 +55,10 @@ export function defineRoleMappingFeatureCheckRoute({ }); } - const enabledFeatures = await getEnabledRoleMappingsFeatures(clusterClient, logger); + const enabledFeatures = await getEnabledRoleMappingsFeatures( + context.core.elasticsearch.client.asInternalUser, + logger + ); return response.ok({ body: { @@ -73,13 +70,12 @@ export function defineRoleMappingFeatureCheckRoute({ ); } -async function getEnabledRoleMappingsFeatures(clusterClient: ILegacyClusterClient, logger: Logger) { +async function getEnabledRoleMappingsFeatures(esClient: ElasticsearchClient, logger: Logger) { logger.debug(`Retrieving role mappings features`); - const nodeScriptSettingsPromise: Promise<NodeSettingsResponse> = clusterClient - .callAsInternalUser('nodes.info', { - filterPath: 'nodes.*.settings.script', - }) + const nodeScriptSettingsPromise = esClient.nodes + .info<NodeSettingsResponse>({ filter_path: 'nodes.*.settings.script' }) + .then(({ body }) => body) .catch((error) => { // fall back to assuming that node settings are unset/at their default values. // this will allow the role mappings UI to permit both role template script types, @@ -88,13 +84,11 @@ async function getEnabledRoleMappingsFeatures(clusterClient: ILegacyClusterClien return {}; }); - const xpackUsagePromise: Promise<XPackUsageResponse> = clusterClient - // `transport.request` is potentially unsafe when combined with untrusted user input. - // Do not augment with such input. - .callAsInternalUser('transport.request', { - method: 'GET', - path: '/_xpack/usage', - }) + // `transport.request` is potentially unsafe when combined with untrusted user input. + // Do not augment with such input. + const xpackUsagePromise = esClient.transport + .request({ method: 'GET', path: '/_xpack/usage' }) + .then(({ body }) => body as XPackUsageResponse) .catch((error) => { // fall back to no external realms configured. // this will cause a warning in the UI about no compatible realms being enabled, but will otherwise allow diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts index 2519034b386bf..625aae42a3907 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts @@ -6,9 +6,9 @@ import Boom from '@hapi/boom'; import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { defineRoleMappingGetRoutes } from './get'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; const mockRoleMappingResponse = { mapping1: { @@ -49,13 +49,22 @@ const mockRoleMappingResponse = { }, }; +function getMockContext( + licenseCheckResult: { state: string; message?: string } = { state: 'valid' } +) { + return { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; +} + describe('GET role mappings', () => { it('returns all role mappings', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(mockRoleMappingResponse); + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue({ + body: mockRoleMappingResponse, + } as any); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -67,11 +76,6 @@ describe('GET role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); @@ -118,29 +122,27 @@ describe('GET role mappings', () => { }, ]); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getRoleMappings', - { name: undefined } - ); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name: undefined }); }); it('returns role mapping by name', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ - mapping1: { - enabled: true, - roles: ['foo', 'bar'], - rules: { - field: { - dn: 'CN=bob,OU=example,O=com', + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue({ + body: { + mapping1: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, }, }, }, - }); + } as any); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -155,11 +157,6 @@ describe('GET role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); @@ -175,16 +172,15 @@ describe('GET role mappings', () => { }, }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getRoleMappings', - { name } - ); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -196,29 +192,19 @@ describe('GET role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).not.toHaveBeenCalled(); }); it('returns a 404 when the role mapping is not found', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockRejectedValue( Boom.notFound('role mapping not found!') ); @@ -235,18 +221,12 @@ describe('GET role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(404); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); expect( - mockScopedClusterClient.callAsCurrentUser - ).toHaveBeenCalledWith('shield.getRoleMappings', { name }); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 63598584b5d1b..5ab9b1f6b4a24 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -14,7 +14,7 @@ interface RoleMappingsResponse { } export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { - const { clusterClient, logger, router } = params; + const { logger, router } = params; router.get( { @@ -29,13 +29,11 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { const expectSingleEntity = typeof request.params.name === 'string'; try { - const roleMappingsResponse: RoleMappingsResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRoleMappings', { - name: request.params.name, - }); + const roleMappingsResponse = await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping<RoleMappingsResponse>( + { name: request.params.name } + ); - const mappings = Object.entries(roleMappingsResponse).map(([name, mapping]) => { + const mappings = Object.entries(roleMappingsResponse.body).map(([name, mapping]) => { return { name, ...mapping, diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts index 8f61d2a122f0c..5dc7a21a02c6f 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts @@ -5,17 +5,20 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { defineRoleMappingPostRoutes } from './post'; describe('POST role mappings', () => { it('allows a role mapping to be created', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ created: true }); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } } as any, + }; + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping.mockResolvedValue({ + body: { created: true }, + } as any); defineRoleMappingPostRoutes(mockRouteDefinitionParams); @@ -39,37 +42,41 @@ describe('POST role mappings', () => { }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual({ created: true }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.saveRoleMapping', - { - name, - body: { - enabled: true, - roles: ['foo', 'bar'], - rules: { - field: { - dn: 'CN=bob,OU=example,O=com', - }, + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name, + body: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', }, }, - } - ); + }, + }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: 'invalid', + message: 'test forbidden message', + }), + }, + } as any, + }; defineRoleMappingPostRoutes(mockRouteDefinitionParams); @@ -81,22 +88,14 @@ describe('POST role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts index 11149f38069a7..6c1b19dacb601 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -8,9 +8,7 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; -export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { - const { clusterClient, router } = params; - +export function defineRoleMappingPostRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/role_mapping/{name}', @@ -43,13 +41,10 @@ export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const saveResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.saveRoleMapping', { - name: request.params.name, - body: request.body, - }); - return response.ok({ body: saveResponse }); + const saveResponse = await context.core.elasticsearch.client.asCurrentUser.security.putRoleMapping( + { name: request.params.name, body: request.body } + ); + return response.ok({ body: saveResponse.body }); } catch (error) { const wrappedError = wrapError(error); return response.customError({ diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index c66b5f985cb33..d98c0acb7d86d 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -7,21 +7,20 @@ import { errors } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { - ILegacyClusterClient, + Headers, IRouter, - ILegacyScopedClusterClient, kibanaResponseFactory, RequestHandler, RequestHandlerContext, RouteConfig, - ScopeableRequest, } from '../../../../../../src/core/server'; import { Authentication, AuthenticationResult } from '../../authentication'; import { Session } from '../../session_management'; import { defineChangeUserPasswordRoutes } from './change_password'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { sessionMock } from '../../session_management/session.mock'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -30,19 +29,19 @@ describe('Change password', () => { let router: jest.Mocked<IRouter>; let authc: jest.Mocked<Authentication>; let session: jest.Mocked<PublicMethodsOf<Session>>; - let mockClusterClient: jest.Mocked<ILegacyClusterClient>; - let mockScopedClusterClient: jest.Mocked<ILegacyScopedClusterClient>; let routeHandler: RequestHandler<any, any, any>; let routeConfig: RouteConfig<any, any, any, any>; - let mockContext: RequestHandlerContext; - - function checkPasswordChangeAPICall(username: string, request: ScopeableRequest) { - expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.changePassword', - { username, body: { password: 'new-password' } } + let mockContext: DeeplyMockedKeys<RequestHandlerContext>; + + function checkPasswordChangeAPICall(username: string, headers?: Headers) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword + ).toHaveBeenCalledWith( + { username, body: { password: 'new-password' } }, + headers && { headers } ); } @@ -56,15 +55,10 @@ describe('Change password', () => { authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); session.get.mockResolvedValue(sessionMock.createValue()); - mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockClusterClient = routeParamsMock.clusterClient; - mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - - mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ check: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; + mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } }, + } as any; defineChangeUserPasswordRoutes(routeParamsMock); @@ -114,20 +108,18 @@ describe('Change password', () => { const changePasswordFailure = new (errors.AuthenticationException as any)('Unauthorized', { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(changePasswordFailure); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + changePasswordFailure + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual(changePasswordFailure); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith({ - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + checkPasswordChangeAPICall(username, { + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); @@ -148,16 +140,16 @@ describe('Change password', () => { expect(response.payload).toEqual(loginFailureReason); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); it('returns 500 if password update request fails with non-401 error.', async () => { const failureReason = new Error('Request failed.'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + failureReason + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); @@ -165,10 +157,8 @@ describe('Change password', () => { expect(response.payload).toEqual(failureReason); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); @@ -179,10 +169,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).toHaveBeenCalledTimes(1); @@ -209,10 +197,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).toHaveBeenCalledTimes(1); @@ -230,10 +216,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).not.toHaveBeenCalled(); @@ -249,7 +233,9 @@ describe('Change password', () => { it('returns 500 if password update request fails.', async () => { const failureReason = new Error('Request failed.'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + failureReason + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); @@ -257,7 +243,7 @@ describe('Change password', () => { expect(response.payload).toEqual(failureReason); expect(authc.login).not.toHaveBeenCalled(); - checkPasswordChangeAPICall(username, mockRequest); + checkPasswordChangeAPICall(username); }); it('successfully changes user password.', async () => { @@ -267,7 +253,7 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); expect(authc.login).not.toHaveBeenCalled(); - checkPasswordChangeAPICall(username, mockRequest); + checkPasswordChangeAPICall(username); }); }); }); diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts index be868f841eeeb..66d36b4294883 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -14,12 +14,7 @@ import { } from '../../authentication'; import { RouteDefinitionParams } from '..'; -export function defineChangeUserPasswordRoutes({ - authc, - session, - router, - clusterClient, -}: RouteDefinitionParams) { +export function defineChangeUserPasswordRoutes({ authc, session, router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/password', @@ -43,28 +38,26 @@ export function defineChangeUserPasswordRoutes({ // If user is changing their own password they should provide a proof of knowledge their // current password via sending it in `Authorization: Basic base64(username:current password)` // HTTP header no matter how they logged in to Kibana. - const scopedClusterClient = clusterClient.asScoped( - isUserChangingOwnPassword - ? { - headers: { - ...request.headers, - authorization: new HTTPAuthorizationHeader( - 'Basic', - new BasicHTTPAuthorizationHeaderCredentials( - username, - currentPassword || '' - ).toString() - ).toString(), - }, - } - : request - ); + const options = isUserChangingOwnPassword + ? { + headers: { + ...request.headers, + authorization: new HTTPAuthorizationHeader( + 'Basic', + new BasicHTTPAuthorizationHeaderCredentials( + username, + currentPassword || '' + ).toString() + ).toString(), + }, + } + : undefined; try { - await scopedClusterClient.callAsCurrentUser('shield.changePassword', { - username, - body: { password: newPassword }, - }); + await context.core.elasticsearch.client.asCurrentUser.security.changePassword( + { username, body: { password: newPassword } }, + options + ); } catch (error) { // This may happen only if user's credentials are rejected meaning that current password // isn't correct. diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index 5a3e50bb11d5c..a98848a583500 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -9,7 +9,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}', @@ -28,7 +28,7 @@ export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteD }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient.asScoped(request).callAsCurrentUser('shield.putUser', { + await context.core.elasticsearch.client.asCurrentUser.security.putUser({ username: request.params.username, // Omit `username`, `enabled` and all fields with `null` value. body: Object.fromEntries( diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts index 99a8d5c18ab3d..26a1765b4fbdf 100644 --- a/x-pack/plugins/security/server/routes/users/delete.ts +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -9,7 +9,7 @@ import { RouteDefinitionParams } from '../index'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineDeleteUserRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/users/{username}', @@ -19,9 +19,9 @@ export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitio }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.deleteUser', { username: request.params.username }); + await context.core.elasticsearch.client.asCurrentUser.security.deleteUser({ + username: request.params.username, + }); return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts index 0867910372546..aa6a4f6be8bad 100644 --- a/x-pack/plugins/security/server/routes/users/get.ts +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -9,7 +9,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetUserRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users/{username}', @@ -20,9 +20,13 @@ export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionPa createLicensedRouteHandler(async (context, request, response) => { try { const username = request.params.username; - const users = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getUser', { username })) as Record<string, {}>; + const { + body: users, + } = await context.core.elasticsearch.client.asCurrentUser.security.getUser< + Record<string, {}> + >({ + username, + }); if (!users[username]) { return response.notFound(); diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts index 492ab27ab27ad..3c5ca184f747c 100644 --- a/x-pack/plugins/security/server/routes/users/get_all.ts +++ b/x-pack/plugins/security/server/routes/users/get_all.ts @@ -8,7 +8,7 @@ import { RouteDefinitionParams } from '../index'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetAllUsersRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users', validate: false }, createLicensedRouteHandler(async (context, request, response) => { @@ -16,7 +16,7 @@ export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefiniti return response.ok({ // Return only values since keys (user names) are already duplicated there. body: Object.values( - await clusterClient.asScoped(request).callAsCurrentUser('shield.getUser') + (await context.core.elasticsearch.client.asCurrentUser.security.getUser()).body ), }); } catch (error) { From 0c623b6c017fc0addbdf18afb7b03e6639538020 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <k.gallagher.05@gmail.com> Date: Wed, 2 Dec 2020 14:13:20 +0000 Subject: [PATCH 056/107] [Synthetics] Waterfall chart (#84199) * Add a Waterfall component - Adds a generic Waterfall component - Adds consumer consumption code for Synthetics --- .../monitor/synthetics/executed_journey.tsx | 54 +- .../monitor/synthetics/waterfall/README.md | 123 ++++ .../waterfall/components/constants.ts | 12 + .../waterfall/components/legend.tsx | 28 + .../components/middle_truncated_text.test.tsx | 39 + .../components/middle_truncated_text.tsx | 61 ++ .../waterfall/components/sidebar.tsx | 43 ++ .../synthetics/waterfall/components/styles.ts | 89 +++ .../waterfall/components/waterfall_chart.tsx | 194 +++++ .../synthetics/data_formatting.test.ts | 687 ++++++++++++++++++ .../consumers/synthetics/data_formatting.ts | 336 +++++++++ .../waterfall/consumers/synthetics/types.ts | 197 +++++ .../synthetics/waterfall_chart_wrapper.tsx | 84 +++ .../waterfall/context/waterfall_chart.tsx | 44 ++ .../monitor/synthetics/waterfall/index.tsx | 10 + .../monitor/synthetics/waterfall/types.ts | 21 + 16 files changed, 1996 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 9a3e045017f9a..0c47e4c73e976 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -57,29 +57,31 @@ interface ExecutedJourneyProps { journey: JourneyState; } -export const ExecutedJourney: FC<ExecutedJourneyProps> = ({ journey }) => ( - <div> - <EuiText> - <h3> - <FormattedMessage - id="xpack.uptime.synthetics.executedJourney.heading" - defaultMessage="Summary information" - /> - </h3> - <p> - {statusMessage( - journey.steps - .filter(isStepEnd) - .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) - )} - </p> - </EuiText> - <EuiSpacer /> - <EuiFlexGroup direction="column"> - {journey.steps.filter(isStepEnd).map((step, index) => ( - <ExecutedStep key={index} index={index} step={step} /> - ))} - <EuiSpacer size="s" /> - </EuiFlexGroup> - </div> -); +export const ExecutedJourney: FC<ExecutedJourneyProps> = ({ journey }) => { + return ( + <div> + <EuiText> + <h3> + <FormattedMessage + id="xpack.uptime.synthetics.executedJourney.heading" + defaultMessage="Summary information" + /> + </h3> + <p> + {statusMessage( + journey.steps + .filter(isStepEnd) + .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) + )} + </p> + </EuiText> + <EuiSpacer /> + <EuiFlexGroup direction="column"> + {journey.steps.filter(isStepEnd).map((step, index) => ( + <ExecutedStep key={index} index={index} step={step} /> + ))} + <EuiSpacer size="s" /> + </EuiFlexGroup> + </div> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md new file mode 100644 index 0000000000000..cf8d3b5345eaa --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md @@ -0,0 +1,123 @@ +# Waterfall chart + +## Introduction + +The waterfall chart component aims to be agnostic in it's approach, so that a variety of consumers / solutions can use it. Some of Elastic Chart's features are used in a non-standard way to facilitate this flexibility, this README aims to cover some of the things that might be less obvious, and also provides a high level overview of implementation. + +## Requirements for usage + +The waterfall chart component asssumes that the consumer is making use of `KibanaReactContext`, and as such things like `useKibana` can be called. + +Consumers are also expected to be using the `<EuiThemeProvider />` so that the waterfall chart can apply styled-component styles based on the EUI theme. + +These are the two hard requirements, but almost all plugins will be using these. + +## Rendering + +At it's core the watefall chart is a stacked bar chart that has been rotated through 90 degrees. As such it's important to understand that `x` is now represented as `y` and vice versa. + +## Flexibility + +This section aims to cover some things that are non-standard. + +### Tooltip + +By default the formatting of tooltip values is very basic, but for a waterfall chart there needs to be a great deal of flexibility to represent whatever breakdown you're trying to show. + +As such a custom tooltip component is used. This custom component would usually only have access to some basic props that pertain to the values of the hovered bar. The waterfall chart component extends this by making us of a waterfall chart context. + +The custom tooltip component can use the context to access the full set of chart data, find the relevant items (those with the same `x` value) and call a custom `renderTooltipItem` for each item, `renderTooltipItem` will be passed `item.config.tooltipProps`. Every consumer can choose what they use for their `tooltipProps`. + +Some consumers might need colours, some might need iconography and so on. The waterfall chart doesn't make assumptions, and will render out the React content returned by `renderTooltipItem`. + +IMPORTANT: `renderTooltipItem` is provided via context and not as a direct prop due to the fact the custom tooltip component would usually only have access to the props provided directly to it from Elastic Charts. + +### Colours + +The easiest way to facilitate specific colours for each stack (let's say your colours are mapped to a constraint like mime type) is to assign the colour directly on your datum `config` property, and then access this directly in the `barStyleAccessor` function, e.g. + +``` +barStyleAccessor={(datum) => { + return datum.datum.config.colour; +}) +``` + +### Config + +The notion of `config` has been mentioned already. But this is a place that consumers can store their solution specific properties. `renderTooltipItem` will make use of `config.tooltipProps`, and `barStyleAccessor` can make use of anything on `config`. + +### Sticky top axis + +By default there is no "sticky" axis functionality in Elastic Charts, therefore a second chart is rendered, this contains a replica of the top axis, and renders one empty data point (as a chart can't only have an axis). This second chart is then positioned in such a way that it covers the top of the real axis, and remains fixed. + +## Data + +The waterfall chart expects data in a relatively simple format, there are the usual plot properties (`x`, `y0`, and `y`) and then `config`. E.g. + +``` +const series = [ + {x: 0, y: 0, y: 100, config: { tooltipProps: { type: 'dns' }}}, + {x: 0, y0: 300, y: 500, config: { tooltipProps: { type: 'ssl' }}}, + {x: 1, y0: 250, y: 300, config: { tooltipProps: { propA: 'somethingBreakdownRelated' }}}, + {x: 1, y0: 500, y: 600, config: { tooltipProps: { propA: 'anotherBreakdown' }}}, +] +``` + +Gaps in bars are fine, and to be expected for certain solutions. + +## Sidebar items + +The waterfall chart component again doesn't make assumptions about consumer's sidebar items' content, but the waterfall chart does handle the rendering so the sidebar can be aligned and rendered properly alongside the chart itself. + +`sidebarItems` should be provided to the context, and a `renderSidebarItem` prop should be provided to the chart. + +A sidebar is optional. + +There is a great deal of flexibility here so that solutions can make use of this in the way they need. For example, if you'd like to add a toggle functionality, so that clicking an item shows / hides it's children, this would involve rendering your toggle in `renderSidebarItem` and then when clicked you can handle adjusting your data as necessary. + +IMPORTANT: It is important to understand that the chart itself makes use of a fixed height. The sidebar will create a space that has a matching height. Each item is assigned equal space vertically via Flexbox, so that the items align with the relevant bar to the right (these are two totally different rendering contexts, with the chart itself sitting within a `canvas` element). So it's important that whatever content you choose to render here doesn't exceed the available height available to each item. The chart's height is calculated as `numberOfBars * 32`, so content should be kept within that `32px` threshold. + +## Legend items + +Much the same as with the sidebar items, no assumptions are made here, solutions will have different aims. + +`legendItems` should be provided to the context, and a `renderLegendItem` prop should be provided to the chart. + +A legend is optional. + +## Overall usage + +Pulling all of this together, things look like this (for a specific solution): + +``` +const renderSidebarItem: RenderItem<SidebarItem> = (item, index) => { + return <MiddleTruncatedText text={`${index + 1}. ${item.url}`} />; +}; + +const renderLegendItem: RenderItem<LegendItem> = (item) => { + return <EuiHealth color={item.colour}>{item.name}</EuiHealth>; +}; + +<WaterfallProvider + data={series} + sidebarItems={sidebarItems} + legendItems={legendItems} + renderTooltipItem={(tooltipProps) => { + return <EuiHealth color={String(tooltipProps.colour)}>{tooltipProps.value}</EuiHealth>; + }} +> + <WaterfallChart + tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} + domain={{ min: domain.min, max: domain.max }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={renderSidebarItem} + renderLegendItem={renderLegendItem} + /> +</WaterfallProvider> +``` + +A solution could easily forego a sidebar and legend for a more minimalistic view, e.g. maybe a mini waterfall within a table column. + + diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts new file mode 100644 index 0000000000000..ac650c5ef0ddd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.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; + * you may not use this file except in compliance with the Elastic License. + */ + +// Pixel value +export const BAR_HEIGHT = 32; +// Flex grow value +export const MAIN_GROW_SIZE = 8; +// Flex grow value +export const SIDEBAR_GROW_SIZE = 2; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx new file mode 100644 index 0000000000000..85a205a7256f3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { IWaterfallContext } from '../context/waterfall_chart'; +import { WaterfallChartLegendContainer } from './styles'; +import { WaterfallChartProps } from './waterfall_chart'; + +interface LegendProps { + items: Required<IWaterfallContext>['legendItems']; + render: Required<WaterfallChartProps>['renderLegendItem']; +} + +export const Legend: React.FC<LegendProps> = ({ items, render }) => { + return ( + <WaterfallChartLegendContainer> + <EuiFlexGroup gutterSize="none"> + {items.map((item, index) => { + return <EuiFlexItem key={index}>{render(item, index)}</EuiFlexItem>; + })} + </EuiFlexGroup> + </WaterfallChartLegendContainer> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx new file mode 100644 index 0000000000000..4f54a347d22d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getChunks, MiddleTruncatedText } from './middle_truncated_text'; +import { shallowWithIntl } from '@kbn/test/jest'; +import React from 'react'; + +const longString = + 'this-is-a-really-really-really-really-really-really-really-really-long-string.madeup.extension'; + +describe('getChunks', () => { + it('Calculates chunks correctly', () => { + const result = getChunks(longString); + expect(result).toEqual({ + first: 'this-is-a-really-really-really-really-really-really-really-really-long-string.made', + last: 'up.extension', + }); + }); +}); + +describe('Component', () => { + it('Renders correctly', () => { + expect(shallowWithIntl(<MiddleTruncatedText text={longString} />)).toMatchInlineSnapshot(` + <styled.div> + <styled.div> + <styled.span> + this-is-a-really-really-really-really-really-really-really-really-long-string.made + </styled.span> + <styled.span> + up.extension + </styled.span> + </styled.div> + </styled.div> + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx new file mode 100644 index 0000000000000..519927d7db28b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +const OuterContainer = styled.div` + width: 100%; + height: 100%; + position: relative; +`; + +const InnerContainer = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; + display: flex; + min-width: 0; +`; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist + +const FirstChunk = styled.span` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +const LastChunk = styled.span` + flex-shrink: 0; +`; + +export const getChunks = (text: string) => { + const END_CHARS = 12; + const chars = text.split(''); + const splitPoint = chars.length - END_CHARS > 0 ? chars.length - END_CHARS : null; + const endChars = splitPoint ? chars.splice(splitPoint) : []; + return { first: chars.join(''), last: endChars.join('') }; +}; + +// Helper component for adding middle text truncation, e.g. +// really-really-really-long....ompressed.js +// Can be used to accomodate content in sidebar item rendering. +export const MiddleTruncatedText = ({ text }: { text: string }) => { + const chunks = useMemo(() => { + return getChunks(text); + }, [text]); + + return ( + <OuterContainer> + <InnerContainer> + <FirstChunk>{chunks.first}</FirstChunk> + <LastChunk>{chunks.last}</LastChunk> + </InnerContainer> + </OuterContainer> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx new file mode 100644 index 0000000000000..9ff544fc1946b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { SIDEBAR_GROW_SIZE } from './constants'; +import { IWaterfallContext } from '../context/waterfall_chart'; +import { + WaterfallChartSidebarContainer, + WaterfallChartSidebarContainerInnerPanel, + WaterfallChartSidebarContainerFlexGroup, + WaterfallChartSidebarFlexItem, +} from './styles'; +import { WaterfallChartProps } from './waterfall_chart'; + +interface SidebarProps { + items: Required<IWaterfallContext>['sidebarItems']; + height: number; + render: Required<WaterfallChartProps>['renderSidebarItem']; +} + +export const Sidebar: React.FC<SidebarProps> = ({ items, height, render }) => { + return ( + <EuiFlexItem grow={SIDEBAR_GROW_SIZE}> + <WaterfallChartSidebarContainer height={height}> + <WaterfallChartSidebarContainerInnerPanel paddingSize="none"> + <WaterfallChartSidebarContainerFlexGroup direction="column" gutterSize="none"> + {items.map((item, index) => { + return ( + <WaterfallChartSidebarFlexItem key={index}> + {render(item, index)} + </WaterfallChartSidebarFlexItem> + ); + })} + </WaterfallChartSidebarContainerFlexGroup> + </WaterfallChartSidebarContainerInnerPanel> + </WaterfallChartSidebarContainer> + </EuiFlexItem> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts new file mode 100644 index 0000000000000..25f5e5f8f5cc9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../observability/public'; + +// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. +const FIXED_AXIS_HEIGHT = 33; + +interface WaterfallChartOuterContainerProps { + height?: number; +} + +export const WaterfallChartOuterContainer = euiStyled.div<WaterfallChartOuterContainerProps>` + height: ${(props) => (props.height ? `${props.height}px` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled.div` + position: sticky; + top: 0; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` + height: 100%; + border-radius: 0 !important; + border: none; +`; // NOTE: border-radius !important is here as the "border" prop isn't working + +export const WaterfallChartFixedAxisContainer = euiStyled.div` + height: ${FIXED_AXIS_HEIGHT}px; +`; + +interface WaterfallChartSidebarContainer { + height: number; +} + +export const WaterfallChartSidebarContainer = euiStyled.div<WaterfallChartSidebarContainer>` + height: ${(props) => `${props.height - FIXED_AXIS_HEIGHT}px`}; + overflow-y: hidden; +`; + +export const WaterfallChartSidebarContainerInnerPanel = euiStyled(EuiPanel)` + border: 0; + height: 100%; +`; + +export const WaterfallChartSidebarContainerFlexGroup = euiStyled(EuiFlexGroup)` + height: 100%; +`; + +// Ensures flex items honour no-wrap of children, rather than trying to extend to the full width of children. +export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` + min-width: 0; + padding-left: ${(props) => props.theme.eui.paddingSizes.m}; + padding-right: ${(props) => props.theme.eui.paddingSizes.m}; +`; + +interface WaterfallChartChartContainer { + height: number; +} + +export const WaterfallChartChartContainer = euiStyled.div<WaterfallChartChartContainer>` + width: 100%; + height: ${(props) => `${props.height}px`}; + margin-top: -${FIXED_AXIS_HEIGHT}px; +`; + +export const WaterfallChartLegendContainer = euiStyled.div` + position: sticky; + bottom: 0; + z-index: ${(props) => props.theme.eui.euiZLevel4}; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + box-shadow: 0px -1px 4px 0px ${(props) => props.theme.eui.euiColorLightShade}; +`; // NOTE: EuiShadowColor is a little too dark to work with the background-color + +export const WaterfallChartTooltip = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorDarkestShade}; + border-radius: ${(props) => props.theme.eui.euiBorderRadius}; + color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.s}; +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx new file mode 100644 index 0000000000000..de4be0ea34b2c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -0,0 +1,194 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + Axis, + BarSeries, + Chart, + Position, + ScaleType, + Settings, + TickFormatter, + DomainRange, + BarStyleAccessor, + TooltipInfo, + TooltipType, +} from '@elastic/charts'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc +// can therefore be accessed. +import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useWaterfallContext } from '../context/waterfall_chart'; +import { + WaterfallChartOuterContainer, + WaterfallChartFixedTopContainer, + WaterfallChartFixedTopContainerSidebarCover, + WaterfallChartFixedAxisContainer, + WaterfallChartChartContainer, + WaterfallChartTooltip, +} from './styles'; +import { WaterfallData } from '../types'; +import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { Sidebar } from './sidebar'; +import { Legend } from './legend'; + +const Tooltip = ({ header }: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return item.x === header?.value; + }); + return ( + <WaterfallChartTooltip> + <EuiFlexGroup direction="column" gutterSize="none"> + {relevantItems.map((item, index) => { + return ( + <EuiFlexItem key={index}>{renderTooltipItem(item.config.tooltipProps)}</EuiFlexItem> + ); + })} + </EuiFlexGroup> + </WaterfallChartTooltip> + ); +}; + +export type RenderItem<I = any> = (item: I, index: number) => JSX.Element; + +export interface WaterfallChartProps { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; + renderSidebarItem?: RenderItem; + renderLegendItem?: RenderItem; + maxHeight?: number; +} + +const getUniqueBars = (data: WaterfallData) => { + return data.reduce<Set<number>>((acc, item) => { + if (!acc.has(item.x)) { + acc.add(item.x); + return acc; + } else { + return acc; + } + }, new Set()); +}; + +const getChartHeight = (data: WaterfallData): number => getUniqueBars(data).size * BAR_HEIGHT; + +export const WaterfallChart = ({ + tickFormat, + domain, + barStyleAccessor, + renderSidebarItem, + renderLegendItem, + maxHeight = 600, +}: WaterfallChartProps) => { + const { data, sidebarItems, legendItems } = useWaterfallContext(); + + const generatedHeight = useMemo(() => { + return getChartHeight(data); + }, [data]); + + const [darkMode] = useUiSetting$<boolean>('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + const shouldRenderSidebar = + sidebarItems && sidebarItems.length > 0 && renderSidebarItem ? true : false; + const shouldRenderLegend = + legendItems && legendItems.length > 0 && renderLegendItem ? true : false; + + return ( + <WaterfallChartOuterContainer height={maxHeight}> + <> + <WaterfallChartFixedTopContainer> + <EuiFlexGroup gutterSize="none"> + {shouldRenderSidebar && ( + <EuiFlexItem grow={SIDEBAR_GROW_SIZE}> + <WaterfallChartFixedTopContainerSidebarCover paddingSize="none" /> + </EuiFlexItem> + )} + <EuiFlexItem grow={shouldRenderSidebar ? MAIN_GROW_SIZE : true}> + <WaterfallChartFixedAxisContainer> + <Chart className="axis-only-chart"> + <Settings + showLegend={false} + rotation={90} + tooltip={{ type: TooltipType.None }} + theme={theme} + /> + + <Axis + id="time" + position={Position.Top} + tickFormat={tickFormat} + domain={domain} + showGridLines={true} + /> + + <Axis id="values" position={Position.Left} tickFormat={() => ''} /> + + <BarSeries + id="waterfallItems" + xScaleType={ScaleType.Linear} + yScaleType={ScaleType.Linear} + xAccessor="x" + yAccessors={['y']} + y0Accessors={['y0']} + styleAccessor={barStyleAccessor} + data={[{ x: 0, y0: 0, y1: 0 }]} + /> + </Chart> + </WaterfallChartFixedAxisContainer> + </EuiFlexItem> + </EuiFlexGroup> + </WaterfallChartFixedTopContainer> + <EuiFlexGroup gutterSize="none"> + {shouldRenderSidebar && ( + <Sidebar items={sidebarItems!} height={generatedHeight} render={renderSidebarItem!} /> + )} + <EuiFlexItem grow={shouldRenderSidebar ? MAIN_GROW_SIZE : true}> + <WaterfallChartChartContainer height={generatedHeight}> + <Chart className="data-chart"> + <Settings + showLegend={false} + rotation={90} + tooltip={{ customTooltip: Tooltip }} + theme={theme} + /> + + <Axis + id="time" + position={Position.Top} + tickFormat={tickFormat} + domain={domain} + showGridLines={true} + /> + + <Axis id="values" position={Position.Left} tickFormat={() => ''} /> + + <BarSeries + id="waterfallItems" + xScaleType={ScaleType.Linear} + yScaleType={ScaleType.Linear} + xAccessor="x" + yAccessors={['y']} + y0Accessors={['y0']} + styleAccessor={barStyleAccessor} + data={data} + /> + </Chart> + </WaterfallChartChartContainer> + </EuiFlexItem> + </EuiFlexGroup> + {shouldRenderLegend && <Legend items={legendItems!} render={renderLegendItem!} />} + </> + </WaterfallChartOuterContainer> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts new file mode 100644 index 0000000000000..698e6b4be0c4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts @@ -0,0 +1,687 @@ +/* + * 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 { colourPalette } from './data_formatting'; + +// const TEST_DATA = [ +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// }, +// synthetics: { +// index: 7, +// payload: { +// request: { +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// method: 'GET', +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// }, +// status: 200, +// method: 'GET', +// end: 13902.944973, +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// type: 'Script', +// is_navigation_request: false, +// start: 13902.752946, +// response: { +// encoded_data_length: 179, +// protocol: 'h2', +// headers: { +// content_encoding: 'br', +// server: 'cloudflare', +// age: '94838', +// cf_cache_status: 'HIT', +// x_content_type_options: 'nosniff', +// last_modified: 'Wed, 04 Feb 2015 03:25:28 GMT', +// cf_ray: '5e9dbc2bdda2e5a7-MAN', +// content_type: 'application/javascript; charset=utf-8', +// x_cloud_trace_context: 'eec7acc7a6f96b5353ef0d648bf437ac', +// expect_ct: +// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', +// access_control_allow_origin: '*', +// vary: 'Accept-Encoding', +// cache_control: 'public, max-age=31536000', +// date: 'Thu, 29 Oct 2020 14:55:00 GMT', +// cf_request_id: '061673ef6b0000e5a7cd07a000000001', +// etag: 'W/"4f70-NHpXdyWxnckEaeiXalAnXQ+oh4Q"', +// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', +// }, +// remote_i_p_address: '104.16.125.175', +// connection_reused: true, +// timing: { +// dns_start: -1, +// push_end: 0, +// worker_fetch_start: -1, +// worker_respond_with_settled: -1, +// proxy_end: -1, +// worker_start: -1, +// worker_ready: -1, +// send_end: 158.391, +// connect_end: -1, +// connect_start: -1, +// send_start: 157.876, +// proxy_start: -1, +// push_start: 0, +// ssl_end: -1, +// receive_headers_end: 186.885, +// ssl_start: -1, +// request_time: 13902.757525, +// dns_end: -1, +// }, +// connection_id: 17, +// status_text: '', +// remote_port: 443, +// status: 200, +// security_details: { +// valid_to: 1627905600, +// certificate_id: 0, +// key_exchange_group: 'X25519', +// valid_from: 1596326400, +// protocol: 'TLS 1.3', +// issuer: 'Cloudflare Inc ECC CA-3', +// key_exchange: '', +// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], +// signed_certificate_timestamp_list: [], +// certificate_transparency_compliance: 'unknown', +// cipher: 'AES_128_GCM', +// subject_name: 'sni.cloudflaressl.com', +// }, +// mime_type: 'application/javascript', +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// from_prefetch_cache: false, +// from_disk_cache: false, +// security_state: 'secure', +// response_time: 1.603983300513211e12, +// from_service_worker: false, +// }, +// }, +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// }, +// monitor: { +// status: 'up', +// duration: { +// us: 24, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// }, +// event: { +// dataset: 'uptime', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// }, +// monitor: { +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 13, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// }, +// synthetics: { +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// index: 9, +// payload: { +// start: 13902.76168, +// url: 'file:///opt/examples/todos/app/app.js', +// method: 'GET', +// is_navigation_request: false, +// end: 13902.770133, +// request: { +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/app.js', +// method: 'GET', +// }, +// status: 0, +// type: 'Script', +// response: { +// protocol: 'file', +// connection_reused: false, +// mime_type: 'text/javascript', +// security_state: 'secure', +// from_disk_cache: false, +// url: 'file:///opt/examples/todos/app/app.js', +// status_text: '', +// connection_id: 0, +// from_prefetch_cache: false, +// encoded_data_length: -1, +// headers: {}, +// status: 0, +// from_service_worker: false, +// }, +// }, +// }, +// event: { +// dataset: 'uptime', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.000Z', +// monitor: { +// timespan: { +// lt: '2020-10-29T14:56:01.000Z', +// gte: '2020-10-29T14:55:01.000Z', +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 44365, +// }, +// type: 'browser', +// }, +// synthetics: { +// journey: { +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// index: 5, +// payload: { +// status: 0, +// url: 'file:///opt/examples/todos/app/index.html', +// end: 13902.730261, +// request: { +// method: 'GET', +// headers: {}, +// mixed_content_type: 'none', +// initial_priority: 'VeryHigh', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/index.html', +// }, +// method: 'GET', +// response: { +// status: 0, +// connection_id: 0, +// from_disk_cache: false, +// headers: {}, +// encoded_data_length: -1, +// status_text: '', +// from_service_worker: false, +// connection_reused: false, +// url: 'file:///opt/examples/todos/app/index.html', +// remote_port: 0, +// security_state: 'secure', +// protocol: 'file', +// mime_type: 'text/html', +// remote_i_p_address: '', +// from_prefetch_cache: false, +// }, +// start: 13902.726626, +// type: 'Document', +// is_navigation_request: true, +// }, +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.044Z', +// monitor: { +// type: 'browser', +// timespan: { +// lt: '2020-10-29T14:56:01.044Z', +// gte: '2020-10-29T14:55:01.044Z', +// }, +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 10524, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// synthetics: { +// package_version: '0.0.1', +// index: 6, +// payload: { +// status: 200, +// type: 'Stylesheet', +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// method: 'GET', +// start: 13902.75266, +// is_navigation_request: false, +// end: 13902.943835, +// response: { +// remote_i_p_address: '104.16.125.175', +// response_time: 1.603983300511892e12, +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// mime_type: 'text/css', +// protocol: 'h2', +// security_state: 'secure', +// encoded_data_length: 414, +// remote_port: 443, +// status_text: '', +// timing: { +// proxy_start: -1, +// worker_ready: -1, +// worker_fetch_start: -1, +// receive_headers_end: 189.169, +// worker_respond_with_settled: -1, +// connect_end: 160.311, +// worker_start: -1, +// send_start: 161.275, +// dns_start: 0.528, +// send_end: 161.924, +// ssl_end: 160.267, +// proxy_end: -1, +// ssl_start: 29.726, +// request_time: 13902.753988, +// dns_end: 5.212, +// push_end: 0, +// push_start: 0, +// connect_start: 5.212, +// }, +// connection_reused: false, +// from_service_worker: false, +// security_details: { +// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], +// valid_from: 1596326400, +// cipher: 'AES_128_GCM', +// protocol: 'TLS 1.3', +// issuer: 'Cloudflare Inc ECC CA-3', +// valid_to: 1627905600, +// certificate_id: 0, +// key_exchange_group: 'X25519', +// certificate_transparency_compliance: 'unknown', +// key_exchange: '', +// subject_name: 'sni.cloudflaressl.com', +// signed_certificate_timestamp_list: [], +// }, +// connection_id: 17, +// status: 200, +// from_disk_cache: false, +// from_prefetch_cache: false, +// headers: { +// date: 'Thu, 29 Oct 2020 14:55:00 GMT', +// x_cloud_trace_context: '76a4f7b8be185f2ac9aa839de3d6f893', +// cache_control: 'public, max-age=31536000', +// expect_ct: +// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', +// content_type: 'text/css; charset=utf-8', +// age: '627638', +// x_content_type_options: 'nosniff', +// last_modified: 'Sat, 09 Jan 2016 00:57:37 GMT', +// access_control_allow_origin: '*', +// cf_request_id: '061673ef6a0000e5a75a309000000001', +// vary: 'Accept-Encoding', +// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', +// cf_ray: '5e9dbc2bdda1e5a7-MAN', +// content_encoding: 'br', +// etag: 'W/"1921-kYwbQVnRAA2V/L9Gr4SCtUE5LHQ"', +// server: 'cloudflare', +// cf_cache_status: 'HIT', +// }, +// }, +// request: { +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'VeryHigh', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// method: 'GET', +// }, +// }, +// journey: { +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// type: 'journey/network_info', +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// agent: { +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// }, +// synthetics: { +// index: 8, +// payload: { +// method: 'GET', +// type: 'Script', +// response: { +// url: 'file:///opt/examples/todos/app/vue.min.js', +// protocol: 'file', +// connection_id: 0, +// headers: {}, +// mime_type: 'text/javascript', +// from_service_worker: false, +// status_text: '', +// connection_reused: false, +// encoded_data_length: -1, +// from_disk_cache: false, +// security_state: 'secure', +// from_prefetch_cache: false, +// status: 0, +// }, +// is_navigation_request: false, +// request: { +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/vue.min.js', +// method: 'GET', +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// }, +// end: 13902.772783, +// status: 0, +// start: 13902.760644, +// url: 'file:///opt/examples/todos/app/vue.min.js', +// }, +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// }, +// monitor: { +// status: 'up', +// duration: { +// us: 82, +// }, +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// id: 'check that title is present', +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// }, +// ]; + +// const toMillis = (seconds: number) => seconds * 1000; + +// describe('getTimings', () => { +// it('Calculates timings for network events correctly', () => { +// // NOTE: Uses these timings as the file protocol events don't have timing information +// const eventOneTimings = getTimings( +// TEST_DATA[0].synthetics.payload.response.timing!, +// toMillis(TEST_DATA[0].synthetics.payload.start), +// toMillis(TEST_DATA[0].synthetics.payload.end) +// ); +// expect(eventOneTimings).toEqual({ +// blocked: 162.4549999999106, +// connect: -1, +// dns: -1, +// receive: 0.5629999989271255, +// send: 0.5149999999999864, +// ssl: undefined, +// wait: 28.494, +// }); + +// const eventFourTimings = getTimings( +// TEST_DATA[3].synthetics.payload.response.timing!, +// toMillis(TEST_DATA[3].synthetics.payload.start), +// toMillis(TEST_DATA[3].synthetics.payload.end) +// ); +// expect(eventFourTimings).toEqual({ +// blocked: 1.8559999997466803, +// connect: 25.52200000000002, +// dns: 4.683999999999999, +// receive: 0.6780000009983667, +// send: 0.6490000000000009, +// ssl: 130.541, +// wait: 27.245000000000005, +// }); +// }); +// }); + +// describe('getSeriesAndDomain', () => { +// let seriesAndDomain: any; +// let NetworkItems: any; + +// beforeAll(() => { +// NetworkItems = extractItems(TEST_DATA); +// seriesAndDomain = getSeriesAndDomain(NetworkItems); +// }); + +// it('Correctly calculates the domain', () => { +// expect(seriesAndDomain.domain).toEqual({ max: 218.34699999913573, min: 0 }); +// }); + +// it('Correctly calculates the series', () => { +// expect(seriesAndDomain.series).toEqual([ +// { +// config: { colour: '#f3b3a6', tooltipProps: { colour: '#f3b3a6', value: '3.635ms' } }, +// x: 0, +// y: 3.6349999997764826, +// y0: 0, +// }, +// { +// config: { +// colour: '#b9a888', +// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.856ms' }, +// }, +// x: 1, +// y: 27.889999999731778, +// y0: 26.0339999999851, +// }, +// { +// config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 4.684ms' } }, +// x: 1, +// y: 32.573999999731775, +// y0: 27.889999999731778, +// }, +// { +// config: { +// colour: '#da8b45', +// tooltipProps: { colour: '#da8b45', value: 'Connecting: 25.522ms' }, +// }, +// x: 1, +// y: 58.095999999731795, +// y0: 32.573999999731775, +// }, +// { +// config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 130.541ms' } }, +// x: 1, +// y: 188.63699999973178, +// y0: 58.095999999731795, +// }, +// { +// config: { +// colour: '#d36086', +// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.649ms' }, +// }, +// x: 1, +// y: 189.28599999973179, +// y0: 188.63699999973178, +// }, +// { +// config: { +// colour: '#b0c9e0', +// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 27.245ms' }, +// }, +// x: 1, +// y: 216.5309999997318, +// y0: 189.28599999973179, +// }, +// { +// config: { +// colour: '#ca8eae', +// tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 0.678ms' }, +// }, +// x: 1, +// y: 217.20900000073016, +// y0: 216.5309999997318, +// }, +// { +// config: { +// colour: '#b9a888', +// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 162.455ms' }, +// }, +// x: 2, +// y: 188.77500000020862, +// y0: 26.320000000298023, +// }, +// { +// config: { +// colour: '#d36086', +// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.515ms' }, +// }, +// x: 2, +// y: 189.2900000002086, +// y0: 188.77500000020862, +// }, +// { +// config: { +// colour: '#b0c9e0', +// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 28.494ms' }, +// }, +// x: 2, +// y: 217.7840000002086, +// y0: 189.2900000002086, +// }, +// { +// config: { +// colour: '#9170b8', +// tooltipProps: { colour: '#9170b8', value: 'Content downloading: 0.563ms' }, +// }, +// x: 2, +// y: 218.34699999913573, +// y0: 217.7840000002086, +// }, +// { +// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '12.139ms' } }, +// x: 3, +// y: 46.15699999965727, +// y0: 34.01799999922514, +// }, +// { +// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '8.453ms' } }, +// x: 4, +// y: 43.506999999284744, +// y0: 35.053999999538064, +// }, +// ]); +// }); +// }); + +describe('Palettes', () => { + it('A colour palette comprising timing and mime type colours is correctly generated', () => { + expect(colourPalette).toEqual({ + blocked: '#b9a888', + connect: '#da8b45', + dns: '#54b399', + font: '#aa6556', + html: '#f3b3a6', + media: '#d6bf57', + other: '#e7664c', + receive: '#54b399', + script: '#9170b8', + send: '#d36086', + ssl: '#edc5a2', + stylesheet: '#ca8eae', + wait: '#b0c9e0', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts new file mode 100644 index 0000000000000..9c66ea638c942 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts @@ -0,0 +1,336 @@ +/* + * 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 { euiPaletteColorBlind } from '@elastic/eui'; + +import { + PayloadTimings, + CalculatedTimings, + NetworkItems, + FriendlyTimingLabels, + FriendlyMimetypeLabels, + MimeType, + MimeTypesMap, + Timings, + TIMING_ORDER, + SidebarItems, + LegendItems, +} from './types'; +import { WaterfallData } from '../../../waterfall'; + +const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); + +// The timing calculations here are based off several sources: +// https://github.com/ChromeDevTools/devtools-frontend/blob/2fe91adefb2921b4deb2b4b125370ef9ccdb8d1b/front_end/sdk/HARLog.js#L307 +// and +// https://chromium.googlesource.com/chromium/blink.git/+/master/Source/devtools/front_end/sdk/HAREntry.js#131 +// and +// https://github.com/cyrus-and/chrome-har-capturer/blob/master/lib/har.js#L195 +// Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end + +export const getTimings = ( + timings: PayloadTimings, + requestSentTime: number, + responseReceivedTime: number +): CalculatedTimings => { + if (!timings) return { blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1 }; + + const getLeastNonNegative = (values: number[]) => + values.reduce<number>((best, value) => (value >= 0 && value < best ? value : best), Infinity); + const getOptionalTiming = (_timings: PayloadTimings, key: keyof PayloadTimings) => + _timings[key] >= 0 ? _timings[key] : -1; + + // NOTE: Request sent and request start can differ due to queue times + const requestStartTime = microToMillis(timings.request_time); + + // Queued + const queuedTime = requestSentTime < requestStartTime ? requestStartTime - requestSentTime : -1; + + // Blocked + // "blocked" represents both queued time + blocked/stalled time + proxy time (ie: anything before the request was actually started). + let blocked = queuedTime; + + const blockedStart = getLeastNonNegative([ + timings.dns_start, + timings.connect_start, + timings.send_start, + ]); + + if (blockedStart !== Infinity) { + blocked += blockedStart; + } + + // Proxy + // Proxy is part of blocked, but it can be quirky in that blocked can be -1 even though there are proxy timings. This can happen with + // protocols like Quic. + if (timings.proxy_end !== -1) { + const blockedProxy = timings.proxy_end - timings.proxy_start; + + if (blockedProxy && blockedProxy > blocked) { + blocked = blockedProxy; + } + } + + // DNS + const dnsStart = timings.dns_end >= 0 ? blockedStart : 0; + const dnsEnd = getOptionalTiming(timings, 'dns_end'); + const dns = dnsEnd - dnsStart; + + // SSL + const sslStart = getOptionalTiming(timings, 'ssl_start'); + const sslEnd = getOptionalTiming(timings, 'ssl_end'); + let ssl; + + if (sslStart >= 0 && sslEnd >= 0) { + ssl = timings.ssl_end - timings.ssl_start; + } + + // Connect + let connect = -1; + if (timings.connect_start >= 0) { + connect = timings.send_start - timings.connect_start; + } + + // Send + const send = timings.send_end - timings.send_start; + + // Wait + const wait = timings.receive_headers_end - timings.send_end; + + // Receive + const receive = responseReceivedTime - (requestStartTime + timings.receive_headers_end); + + // SSL connection is a part of the overall connection time + if (connect && ssl) { + connect = connect - ssl; + } + + return { blocked, dns, connect, send, wait, receive, ssl }; +}; + +// TODO: Switch to real API data, and type data as the payload response (if server response isn't preformatted) +export const extractItems = (data: any): NetworkItems => { + const items = data + .map((entry: any) => { + const requestSentTime = microToMillis(entry.synthetics.payload.start); + const responseReceivedTime = microToMillis(entry.synthetics.payload.end); + const requestStartTime = + entry.synthetics.payload.response && entry.synthetics.payload.response.timing + ? microToMillis(entry.synthetics.payload.response.timing.request_time) + : null; + + return { + timestamp: entry['@timestamp'], + method: entry.synthetics.payload.method, + url: entry.synthetics.payload.url, + status: entry.synthetics.payload.status, + mimeType: entry.synthetics.payload?.response?.mime_type, + requestSentTime, + responseReceivedTime, + earliestRequestTime: requestStartTime + ? Math.min(requestSentTime, requestStartTime) + : requestSentTime, + timings: + entry.synthetics.payload.response && entry.synthetics.payload.response.timing + ? getTimings( + entry.synthetics.payload.response.timing, + requestSentTime, + responseReceivedTime + ) + : null, + }; + }) + .sort((a: any, b: any) => { + return a.earliestRequestTime - b.earliestRequestTime; + }); + + return items; +}; + +const formatValueForDisplay = (value: number, points: number = 3) => { + return Number(value).toFixed(points); +}; + +const getColourForMimeType = (mimeType?: string) => { + const key = mimeType && MimeTypesMap[mimeType] ? MimeTypesMap[mimeType] : MimeType.Other; + return colourPalette[key]; +}; + +export const getSeriesAndDomain = (items: NetworkItems) => { + // The earliest point in time a request is sent or started. This will become our notion of "0". + const zeroOffset = items.reduce<number>((acc, item) => { + const { earliestRequestTime } = item; + return earliestRequestTime < acc ? earliestRequestTime : acc; + }, Infinity); + + const series = items.reduce<WaterfallData>((acc, item, index) => { + const { earliestRequestTime } = item; + + // Entries without timings should be handled differently: + // https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L140 + // If there are no concrete timings just plot one block via start and end + if (!item.timings || item.timings === null) { + const duration = item.responseReceivedTime - item.earliestRequestTime; + const colour = getColourForMimeType(item.mimeType); + return [ + ...acc, + { + x: index, + y0: item.earliestRequestTime - zeroOffset, + y: item.responseReceivedTime - zeroOffset, + config: { + colour, + tooltipProps: { + value: `${formatValueForDisplay(duration)}ms`, + colour, + }, + }, + }, + ]; + } + + let currentOffset = earliestRequestTime - zeroOffset; + + TIMING_ORDER.forEach((timing) => { + const value = item.timings![timing]; + const colour = + timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; + if (value && value >= 0) { + const y = currentOffset + value; + + acc.push({ + x: index, + y0: currentOffset, + y, + config: { + colour, + tooltipProps: { + value: `${FriendlyTimingLabels[timing]}: ${formatValueForDisplay( + y - currentOffset + )}ms`, + colour, + }, + }, + }); + currentOffset = y; + } + }); + return acc; + }, []); + + const yValues = series.map((serie) => serie.y); + const domain = { min: 0, max: Math.max(...yValues) }; + return { series, domain }; +}; + +export const getSidebarItems = (items: NetworkItems): SidebarItems => { + return items.map((item) => { + const { url, status, method } = item; + return { url, status, method }; + }); +}; + +export const getLegendItems = (): LegendItems => { + let timingItems: LegendItems = []; + Object.values(Timings).forEach((timing) => { + // The "receive" timing is mapped to a mime type colour, so we don't need to show this in the legend + if (timing === Timings.Receive) { + return; + } + timingItems = [ + ...timingItems, + { name: FriendlyTimingLabels[timing], colour: TIMING_PALETTE[timing] }, + ]; + }); + + let mimeTypeItems: LegendItems = []; + Object.values(MimeType).forEach((mimeType) => { + mimeTypeItems = [ + ...mimeTypeItems, + { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, + ]; + }); + return [...timingItems, ...mimeTypeItems]; +}; + +// Timing colour palette +type TimingColourPalette = { + [K in Timings]: string; +}; + +const SAFE_PALETTE = euiPaletteColorBlind({ rotations: 2 }); + +const buildTimingPalette = (): TimingColourPalette => { + const palette = Object.values(Timings).reduce<Partial<TimingColourPalette>>((acc, value) => { + switch (value) { + case Timings.Blocked: + acc[value] = SAFE_PALETTE[6]; + break; + case Timings.Dns: + acc[value] = SAFE_PALETTE[0]; + break; + case Timings.Connect: + acc[value] = SAFE_PALETTE[7]; + break; + case Timings.Ssl: + acc[value] = SAFE_PALETTE[17]; + break; + case Timings.Send: + acc[value] = SAFE_PALETTE[2]; + break; + case Timings.Wait: + acc[value] = SAFE_PALETTE[11]; + break; + case Timings.Receive: + acc[value] = SAFE_PALETTE[0]; + break; + } + return acc; + }, {}); + + return palette as TimingColourPalette; +}; + +const TIMING_PALETTE = buildTimingPalette(); + +// MimeType colour palette +type MimeTypeColourPalette = { + [K in MimeType]: string; +}; + +const buildMimeTypePalette = (): MimeTypeColourPalette => { + const palette = Object.values(MimeType).reduce<Partial<MimeTypeColourPalette>>((acc, value) => { + switch (value) { + case MimeType.Html: + acc[value] = SAFE_PALETTE[19]; + break; + case MimeType.Script: + acc[value] = SAFE_PALETTE[3]; + break; + case MimeType.Stylesheet: + acc[value] = SAFE_PALETTE[4]; + break; + case MimeType.Media: + acc[value] = SAFE_PALETTE[5]; + break; + case MimeType.Font: + acc[value] = SAFE_PALETTE[8]; + break; + case MimeType.Other: + acc[value] = SAFE_PALETTE[9]; + break; + } + return acc; + }, {}); + + return palette as MimeTypeColourPalette; +}; + +const MIME_TYPE_PALETTE = buildMimeTypePalette(); + +type ColourPalette = TimingColourPalette & MimeTypeColourPalette; + +export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PALETTE }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts new file mode 100644 index 0000000000000..1dd58b4f86db3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export enum Timings { + Blocked = 'blocked', + Dns = 'dns', + Connect = 'connect', + Ssl = 'ssl', + Send = 'send', + Wait = 'wait', + Receive = 'receive', +} + +export const FriendlyTimingLabels = { + [Timings.Blocked]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.blocked', + { + defaultMessage: 'Queued / Blocked', + } + ), + [Timings.Dns]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.dns', { + defaultMessage: 'DNS', + }), + [Timings.Connect]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.connect', + { + defaultMessage: 'Connecting', + } + ), + [Timings.Ssl]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.ssl', { + defaultMessage: 'SSL', + }), + [Timings.Send]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.send', { + defaultMessage: 'Sending request', + }), + [Timings.Wait]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.wait', { + defaultMessage: 'Waiting (TTFB)', + }), + [Timings.Receive]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.receive', + { + defaultMessage: 'Content downloading', + } + ), +}; + +export const TIMING_ORDER = [ + Timings.Blocked, + Timings.Dns, + Timings.Connect, + Timings.Ssl, + Timings.Send, + Timings.Wait, + Timings.Receive, +] as const; + +export type CalculatedTimings = { + [K in Timings]?: number; +}; + +export enum MimeType { + Html = 'html', + Script = 'script', + Stylesheet = 'stylesheet', + Media = 'media', + Font = 'font', + Other = 'other', +} + +export const FriendlyMimetypeLabels = { + [MimeType.Html]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.html', { + defaultMessage: 'HTML', + }), + [MimeType.Script]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.script', + { + defaultMessage: 'JS', + } + ), + [MimeType.Stylesheet]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.stylesheet', + { + defaultMessage: 'CSS', + } + ), + [MimeType.Media]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.media', + { + defaultMessage: 'Media', + } + ), + [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { + defaultMessage: 'Font', + }), + [MimeType.Other]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', + { + defaultMessage: 'Other', + } + ), +}; + +// NOTE: This list tries to cover the standard spec compliant mime types, +// and a few popular non-standard ones, but it isn't exhaustive. +export const MimeTypesMap: Record<string, MimeType> = { + 'text/html': MimeType.Html, + 'application/javascript': MimeType.Script, + 'text/javascript': MimeType.Script, + 'text/css': MimeType.Stylesheet, + // Images + 'image/apng': MimeType.Media, + 'image/bmp': MimeType.Media, + 'image/gif': MimeType.Media, + 'image/x-icon': MimeType.Media, + 'image/jpeg': MimeType.Media, + 'image/png': MimeType.Media, + 'image/svg+xml': MimeType.Media, + 'image/tiff': MimeType.Media, + 'image/webp': MimeType.Media, + // Common audio / video formats + 'audio/wave': MimeType.Media, + 'audio/wav': MimeType.Media, + 'audio/x-wav': MimeType.Media, + 'audio/x-pn-wav': MimeType.Media, + 'audio/webm': MimeType.Media, + 'video/webm': MimeType.Media, + 'audio/ogg': MimeType.Media, + 'video/ogg': MimeType.Media, + 'application/ogg': MimeType.Media, + // Fonts + 'font/otf': MimeType.Font, + 'font/ttf': MimeType.Font, + 'font/woff': MimeType.Font, + 'font/woff2': MimeType.Font, + 'application/x-font-opentype': MimeType.Font, + 'application/font-woff': MimeType.Font, + 'application/font-woff2': MimeType.Font, + 'application/vnd.ms-fontobject': MimeType.Font, + 'application/font-sfnt': MimeType.Font, +}; + +export interface NetworkItem { + timestamp: string; + method: string; + url: string; + status: number; + mimeType?: string; + // NOTE: This is the time the request was actually issued. timing.request_time might be later if the request was queued. + requestSentTime: number; + responseReceivedTime: number; + // NOTE: Denotes the earlier figure out of request sent time and request start time (part of timings). This can vary based on queue times, and + // also whether an entry actually has timings available. + // Ref: https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L154 + earliestRequestTime: number; + timings: CalculatedTimings | null; +} +export type NetworkItems = NetworkItem[]; + +// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. +export interface PayloadTimings { + dns_start: number; + push_end: number; + worker_fetch_start: number; + worker_respond_with_settled: number; + proxy_end: number; + worker_start: number; + worker_ready: number; + send_end: number; + connect_end: number; + connect_start: number; + send_start: number; + proxy_start: number; + push_start: number; + ssl_end: number; + receive_headers_end: number; + ssl_start: number; + request_time: number; + dns_end: number; +} + +export interface ExtraSeriesConfig { + colour: string; +} + +export type SidebarItem = Pick<NetworkItem, 'url' | 'status' | 'method'>; +export type SidebarItems = SidebarItem[]; + +export interface LegendItem { + name: string; + colour: string; +} +export type LegendItems = LegendItem[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx new file mode 100644 index 0000000000000..434b44a94b79f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; +import { SidebarItem, LegendItem, NetworkItems } from './types'; +import { + WaterfallProvider, + WaterfallChart, + MiddleTruncatedText, + RenderItem, +} from '../../../waterfall'; + +const renderSidebarItem: RenderItem<SidebarItem> = (item, index) => { + const { status } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + return ( + <> + {!isErrorStatusCode(status) ? ( + <MiddleTruncatedText text={`${index + 1}. ${item.url}`} /> + ) : ( + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <MiddleTruncatedText text={`${index + 1}. ${item.url}`} /> + </EuiFlexItem> + <EuiFlexItem component="span" grow={false}> + <EuiBadge color="danger">{status}</EuiBadge> + </EuiFlexItem> + </EuiFlexGroup> + )} + </> + ); +}; + +const renderLegendItem: RenderItem<LegendItem> = (item) => { + return <EuiHealth color={item.colour}>{item.name}</EuiHealth>; +}; + +export const WaterfallChartWrapper = () => { + // TODO: Will be sourced via an API + const [networkData] = useState<NetworkItems>([]); + + const { series, domain } = useMemo(() => { + return getSeriesAndDomain(networkData); + }, [networkData]); + + const sidebarItems = useMemo(() => { + return getSidebarItems(networkData); + }, [networkData]); + + const legendItems = getLegendItems(); + + return ( + <WaterfallProvider + data={series} + sidebarItems={sidebarItems} + legendItems={legendItems} + renderTooltipItem={(tooltipProps) => { + return <EuiHealth color={String(tooltipProps.colour)}>{tooltipProps.value}</EuiHealth>; + }} + > + <WaterfallChart + tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} + domain={domain} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={renderSidebarItem} + renderLegendItem={renderLegendItem} + /> + </WaterfallProvider> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx new file mode 100644 index 0000000000000..ccee9d7994c80 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, useContext, Context } from 'react'; +import { WaterfallData, WaterfallDataEntry } from '../types'; + +export interface IWaterfallContext { + data: WaterfallData; + sidebarItems?: unknown[]; + legendItems?: unknown[]; + renderTooltipItem: ( + item: WaterfallDataEntry['config']['tooltipProps'], + index?: number + ) => JSX.Element; +} + +export const WaterfallContext = createContext<Partial<IWaterfallContext>>({}); + +interface ProviderProps { + data: IWaterfallContext['data']; + sidebarItems?: IWaterfallContext['sidebarItems']; + legendItems?: IWaterfallContext['legendItems']; + renderTooltipItem: IWaterfallContext['renderTooltipItem']; +} + +export const WaterfallProvider: React.FC<ProviderProps> = ({ + children, + data, + sidebarItems, + legendItems, + renderTooltipItem, +}) => { + return ( + <WaterfallContext.Provider value={{ data, sidebarItems, legendItems, renderTooltipItem }}> + {children} + </WaterfallContext.Provider> + ); +}; + +export const useWaterfallContext = () => + useContext((WaterfallContext as unknown) as Context<IWaterfallContext>); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx new file mode 100644 index 0000000000000..c3ea39a9ace6e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WaterfallChart, RenderItem, WaterfallChartProps } from './components/waterfall_chart'; +export { WaterfallProvider, useWaterfallContext } from './context/waterfall_chart'; +export { MiddleTruncatedText } from './components/middle_truncated_text'; +export { WaterfallData, WaterfallDataEntry } from './types'; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts new file mode 100644 index 0000000000000..d6901fb482599 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface PlotProperties { + x: number; + y: number; + y0: number; +} + +export interface WaterfallDataSeriesConfigProperties { + tooltipProps: Record<string, string | number>; +} + +export type WaterfallDataEntry = PlotProperties & { + config: WaterfallDataSeriesConfigProperties & Record<string, unknown>; +}; + +export type WaterfallData = WaterfallDataEntry[]; From 90a18cc15d71212363fa9081c545c9ce19f27c56 Mon Sep 17 00:00:00 2001 From: John Schulz <john.schulz@elastic.co> Date: Wed, 2 Dec 2020 09:49:21 -0500 Subject: [PATCH 057/107] [Fleet][EPM] Pass through valid manifest values from upload (#84703) * Add missing properties & improve type safety * Break up types for better readability --- .../server/services/epm/archive/validation.ts | 80 ++++++++++++++----- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index dc7a91e08799c..b93a9119cd4df 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -5,7 +5,7 @@ */ import yaml from 'js-yaml'; -import { uniq } from 'lodash'; +import { pick, uniq } from 'lodash'; import { ArchivePackage, RegistryPolicyTemplate, @@ -21,6 +21,42 @@ import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record<string, Buffer> = {}; const MANIFEST_NAME = 'manifest.yml'; +// not sure these are 100% correct but they do the job here +// keeping them local until others need them +type OptionalPropertyOf<T extends object> = Exclude< + { + [K in keyof T]: T extends Record<K, T[K]> ? never : K; + }[keyof T], + undefined +>; +type RequiredPropertyOf<T extends object> = Exclude<keyof T, OptionalPropertyOf<T>>; + +type RequiredPackageProp = RequiredPropertyOf<ArchivePackage>; +type OptionalPackageProp = OptionalPropertyOf<ArchivePackage>; +// pro: guarantee only supplying known values. these keys must be in ArchivePackage. no typos or new values +// pro: any values added to these lists will be passed through by default +// pro & con: values do need to be shadowed / repeated from ArchivePackage, but perhaps we want to limit values +const requiredArchivePackageProps: readonly RequiredPackageProp[] = [ + 'name', + 'version', + 'description', + 'type', + 'categories', + 'format_version', +] as const; + +const optionalArchivePackageProps: readonly OptionalPackageProp[] = [ + 'title', + 'release', + 'readme', + 'screenshots', + 'icons', + 'assets', + 'internal', + 'data_streams', + 'policy_templates', +] as const; + // TODO: everything below performs verification of manifest.yml files, and hence duplicates functionality already implemented in the // package registry. At some point this should probably be replaced (or enhanced) with verification based on // https://github.com/elastic/package-spec/ @@ -58,43 +94,43 @@ function parseAndVerifyArchive(paths: string[]): ArchivePackage { } // ... which must be valid YAML - let manifest; + let manifest: ArchivePackage; try { manifest = yaml.load(manifestBuffer.toString()); } catch (error) { throw new PackageInvalidArchiveError(`Could not parse top-level package manifest: ${error}.`); } - // Package name and version from the manifest must match those from the toplevel directory - const pkgKey = pkgToPkgKey({ name: manifest.name, version: manifest.version }); - if (toplevelDir !== pkgKey) { + // must have mandatory fields + const reqGiven = pick(manifest, requiredArchivePackageProps); + const requiredKeysMatch = + Object.keys(reqGiven).toString() === requiredArchivePackageProps.toString(); + if (!requiredKeysMatch) { + const list = requiredArchivePackageProps.join(', '); throw new PackageInvalidArchiveError( - `Name ${manifest.name} and version ${manifest.version} do not match top-level directory ${toplevelDir}` + `Invalid top-level package manifest: one or more fields missing of ${list}` ); } - const { name, version, description, type, categories, format_version: formatVersion } = manifest; - // check for mandatory fields - if (!(name && version && description && type && categories && formatVersion)) { + // at least have all required properties + // get optional values and combine into one object for the remaining operations + const optGiven = pick(manifest, optionalArchivePackageProps); + const parsed: ArchivePackage = { ...reqGiven, ...optGiven }; + + // Package name and version from the manifest must match those from the toplevel directory + const pkgKey = pkgToPkgKey({ name: parsed.name, version: parsed.version }); + if (toplevelDir !== pkgKey) { throw new PackageInvalidArchiveError( - 'Invalid top-level package manifest: one or more fields missing of name, version, description, type, categories, format_version' + `Name ${parsed.name} and version ${parsed.version} do not match top-level directory ${toplevelDir}` ); } - const dataStreams = parseAndVerifyDataStreams(paths, name, version); - const policyTemplates = parseAndVerifyPolicyTemplates(manifest); + parsed.data_streams = parseAndVerifyDataStreams(paths, parsed.name, parsed.version); + parsed.policy_templates = parseAndVerifyPolicyTemplates(manifest); - return { - name, - version, - description, - type, - categories, - format_version: formatVersion, - data_streams: dataStreams, - policy_templates: policyTemplates, - }; + return parsed; } + function parseAndVerifyDataStreams( paths: string[], pkgName: string, From cc341b32354c71a3d0220328ce4f1787bba3caf0 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky <streamich@gmail.com> Date: Wed, 2 Dec 2020 16:06:18 +0100 Subject: [PATCH 058/107] Telemetry for Dyanmic Actions (Drilldowns) (#84580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 set up telemetry for UiActions * feat: 🎸 improve ui_actions_enhanced collector * feat: 🎸 namespace ui actions telemetry stats * refactor: 💡 improve dynamic actions collector setup * feat: 🎸 add tests for dynamicActionsCollector * feat: 🎸 collect dynamic action trigger statistics * refactor: 💡 standartize metric naming * feat: 🎸 aggregate action x trigger counts * test: 💍 add tests for factory stats * docs: ✏️ add ui actions enhanced telemetry docs * fix: 🐛 revert type change * refactor: 💡 make dynamic action stats global * refactor: 💡 use global telemetry stats in action factories --- x-pack/plugins/ui_actions_enhanced/README.md | 63 ++++ .../server/dynamic_action_enhancement.ts | 15 +- ...dynamic_action_factories_collector.test.ts | 124 ++++++++ .../dynamic_action_factories_collector.ts | 24 ++ .../dynamic_actions_collector.test.ts | 271 ++++++++++++++++++ .../telemetry/dynamic_actions_collector.ts | 36 +++ .../server/telemetry/get_metric_key.ts | 10 + 7 files changed, 536 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts diff --git a/x-pack/plugins/ui_actions_enhanced/README.md b/x-pack/plugins/ui_actions_enhanced/README.md index a4a37b559ff8d..cd2a34a2f7536 100644 --- a/x-pack/plugins/ui_actions_enhanced/README.md +++ b/x-pack/plugins/ui_actions_enhanced/README.md @@ -3,3 +3,66 @@ Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. - [__Dashboard drilldown user docs__](https://www.elastic.co/guide/en/kibana/master/drilldowns.html) + +## Dynamic Actions Telemetry + +Dynamic actions (drilldowns) report telemetry. Below is the summary of dynamic action metrics that are reported using telemetry. + +### Dynamic action count + +Total count of dynamic actions (drilldowns) on a saved object. + +``` +dynamicActions.count +``` + +### Count by factory ID + +Count of active dynamic actions (drilldowns) on a saved object by factory ID (drilldown type). + +``` +dynamicActions.actions.<factory_id>.count +``` + +For example: + +``` +dynamicActions.actions.DASHBOARD_TO_DASHBOARD_DRILLDOWN.count +dynamicActions.actions.URL_DRILLDOWN.count +``` + +### Count by trigger + +Count of active dynamic actions (drilldowns) on a saved object by a trigger to which they are attached. + +``` +dynamicActions.triggers.<trigger>.count +``` + +For example: + +``` +dynamicActions.triggers.VALUE_CLICK_TRIGGER.count +dynamicActions.triggers.RANGE_SELECT_TRIGGER.count +``` + +### Count by factory and trigger + +Count of active dynamic actions (drilldowns) on a saved object by a factory ID and trigger ID. + +``` +dynamicActions.action_triggers.<factory_id>_<trigger>.count +``` + +For example: + +``` +dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_VALUE_CLICK_TRIGGER.count +dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_RANGE_SELECT_TRIGGER.count +dynamicActions.action_triggers.URL_DRILLDOWN_VALUE_CLICK_TRIGGER.count +``` + +### Factory metrics + +Each dynamic action factory (drilldown type) can report its own stats, which is +done using the `.telemetry()` method on dynamic action factories. diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index 4cea7ddf4854a..16e7e7967838d 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -8,19 +8,20 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddabl import { SavedObjectReference } from '../../../../src/core/types'; import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import { dynamicActionsCollector } from './telemetry/dynamic_actions_collector'; +import { dynamicActionFactoriesCollector } from './telemetry/dynamic_action_factories_collector'; export const dynamicActionEnhancement = ( getActionFactory: (id: string) => undefined | ActionFactory ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (state: SerializableState, telemetry: Record<string, any>) => { - let telemetryData = telemetry; - (state as DynamicActionsState).events.forEach((event: SerializedEvent) => { - const factory = getActionFactory(event.action.factoryId); - if (factory) telemetryData = factory.telemetry(event, telemetryData); - }); - return telemetryData; + telemetry: (serializableState: SerializableState, stats: Record<string, any>) => { + const state = serializableState as DynamicActionsState; + stats = dynamicActionsCollector(state, stats); + stats = dynamicActionFactoriesCollector(getActionFactory, state, stats); + + return stats; }, extract: (state: SerializableState) => { const references: SavedObjectReference[] = []; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts new file mode 100644 index 0000000000000..9d38fd9d302a4 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { dynamicActionFactoriesCollector } from './dynamic_action_factories_collector'; +import { DynamicActionsState } from '../../common'; +import { ActionFactory } from '../types'; + +type GetActionFactory = (id: string) => undefined | ActionFactory; + +const factories: Record<string, ActionFactory> = { + FACTORY_ID_1: ({ + id: 'FACTORY_ID_1', + telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => { + stats.myStat_1 = 1; + stats.myStat_2 = 123; + return stats; + }), + } as unknown) as ActionFactory, + FACTORY_ID_2: ({ + id: 'FACTORY_ID_2', + telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => stats), + } as unknown) as ActionFactory, + FACTORY_ID_3: ({ + id: 'FACTORY_ID_3', + telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => { + stats.myStat_1 = 2; + stats.stringStat = 'abc'; + return stats; + }), + } as unknown) as ActionFactory, +}; + +const getActionFactory: GetActionFactory = (id: string) => factories[id]; + +const state: DynamicActionsState = { + events: [ + { + eventId: 'eventId-1', + triggers: ['TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Click me!', + config: {}, + }, + }, + { + eventId: 'eventId-2', + triggers: ['TRIGGER_2', 'TRIGGER_3'], + action: { + factoryId: 'FACTORY_ID_2', + name: 'Click me, too!', + config: { + doCleanup: true, + }, + }, + }, + { + eventId: 'eventId-3', + triggers: ['TRIGGER_4', 'TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_3', + name: 'Go to documentation', + config: { + url: 'http://google.com', + iamFeelingLucky: true, + }, + }, + }, + ], +}; + +beforeEach(() => { + Object.values(factories).forEach((factory) => { + ((factory.telemetry as unknown) as jest.SpyInstance).mockClear(); + }); +}); + +describe('dynamicActionFactoriesCollector', () => { + test('returns empty stats when there are not dynamic actions', () => { + const stats = dynamicActionFactoriesCollector( + getActionFactory, + { + events: [], + }, + {} + ); + + expect(stats).toEqual({}); + }); + + test('calls .telemetry() method of a supplied factory', () => { + const currentState = { + events: [state.events[0]], + }; + dynamicActionFactoriesCollector(getActionFactory, currentState, {}); + + const spy1 = (factories.FACTORY_ID_1.telemetry as unknown) as jest.SpyInstance; + const spy2 = (factories.FACTORY_ID_2.telemetry as unknown) as jest.SpyInstance; + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(0); + + expect(spy1.mock.calls[0][0]).toEqual(currentState.events[0]); + expect(typeof spy1.mock.calls[0][1]).toBe('object'); + expect(!!spy1.mock.calls[0][1]).toBe(true); + }); + + test('returns stats received from factory', () => { + const currentState = { + events: [state.events[0]], + }; + const stats = dynamicActionFactoriesCollector(getActionFactory, currentState, {}); + + expect(stats).toEqual({ + myStat_1: 1, + myStat_2: 123, + }); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts new file mode 100644 index 0000000000000..2ece6102c27a4 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicActionsState } from '../../common'; +import { ActionFactory } from '../types'; + +export const dynamicActionFactoriesCollector = ( + getActionFactory: (id: string) => undefined | ActionFactory, + state: DynamicActionsState, + stats: Record<string, any> +): Record<string, any> => { + for (const event of state.events) { + const factory = getActionFactory(event.action.factoryId); + + if (factory) { + stats = factory.telemetry(event, stats); + } + } + + return stats; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts new file mode 100644 index 0000000000000..99217cd98fa01 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { dynamicActionsCollector } from './dynamic_actions_collector'; +import { DynamicActionsState } from '../../common'; + +const state: DynamicActionsState = { + events: [ + { + eventId: 'eventId-1', + triggers: ['TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Click me!', + config: {}, + }, + }, + { + eventId: 'eventId-2', + triggers: ['TRIGGER_2', 'TRIGGER_3'], + action: { + factoryId: 'FACTORY_ID_2', + name: 'Click me, too!', + config: { + doCleanup: true, + }, + }, + }, + { + eventId: 'eventId-3', + triggers: ['TRIGGER_4', 'TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Go to documentation', + config: { + url: 'http://google.com', + iamFeelingLucky: true, + }, + }, + }, + ], +}; + +describe('dynamicActionsCollector', () => { + describe('dynamic action count', () => { + test('equal to zero when there are no dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 0, + }); + }); + + test('does not update existing count if there are no dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [], + }, + { + 'dynamicActions.count': 25, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 25, + }); + }); + + test('equal to one when there is one dynamic action', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 1, + }); + }); + + test('adds one to the current dynamic action count', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.count': 2, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 3, + }); + }); + + test('equal to three when there are three dynamic action', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[1], state.events[2]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 3, + }); + }); + }); + + describe('registered action counts', () => { + test('for single action sets count to one', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 1, + }); + }); + + test('adds count to existing action counts', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.actions.FACTORY_ID_1.count': 5, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 6, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + }); + }); + + test('aggregates count factory count', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 2, + }); + }); + + test('returns counts for every factory type', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 2, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + }); + }); + }); + + describe('action trigger counts', () => { + test('for single action sets count to one', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 1, + }); + }); + + test('adds count to existing stats', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.triggers.TRIGGER_1.count': 123, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 124, + }); + }); + + test('aggregates trigger counts from all dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 2, + 'dynamicActions.triggers.TRIGGER_2.count': 1, + 'dynamicActions.triggers.TRIGGER_3.count': 1, + 'dynamicActions.triggers.TRIGGER_4.count': 1, + }); + }); + }); + + describe('action x trigger counts', () => { + test('returns single action (factoryId x trigger) stat', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 1, + }); + }); + + test('adds count to existing stats', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 3, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 4, + }); + }); + + test('aggregates actions x triggers counts for all events', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 2, + 'dynamicActions.action_triggers.FACTORY_ID_2_TRIGGER_2.count': 1, + 'dynamicActions.action_triggers.FACTORY_ID_2_TRIGGER_3.count': 1, + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_4.count': 1, + }); + }); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts new file mode 100644 index 0000000000000..ae595776fda58 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicActionsState } from '../../common'; +import { getMetricKey } from './get_metric_key'; + +export const dynamicActionsCollector = ( + state: DynamicActionsState, + stats: Record<string, any> +): Record<string, any> => { + const countMetricKey = getMetricKey('count'); + + stats[countMetricKey] = state.events.length + (stats[countMetricKey] || 0); + + for (const event of state.events) { + const factoryId = event.action.factoryId; + const actionCountMetric = getMetricKey(`actions.${factoryId}.count`); + + stats[actionCountMetric] = 1 + (stats[actionCountMetric] || 0); + + for (const trigger of event.triggers) { + const triggerCountMetric = getMetricKey(`triggers.${trigger}.count`); + const actionXTriggerCountMetric = getMetricKey( + `action_triggers.${factoryId}_${trigger}.count` + ); + + stats[triggerCountMetric] = 1 + (stats[triggerCountMetric] || 0); + stats[actionXTriggerCountMetric] = 1 + (stats[actionXTriggerCountMetric] || 0); + } + } + + return stats; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts new file mode 100644 index 0000000000000..6d3ae370c5200 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const prefix = 'dynamicActions.'; + +/** Returns prefixed telemetry metric key for all dynamic action metrics. */ +export const getMetricKey = (path: string) => `${prefix}${path}`; From d481adc75f29b42a9abd738cf6c7f5c570d2688e Mon Sep 17 00:00:00 2001 From: Bhavya RM <bhavya@elastic.co> Date: Wed, 2 Dec 2020 10:30:27 -0500 Subject: [PATCH 059/107] Test user assignment to embeddable maps tests (#84383) --- .../apps/maps/embeddable/dashboard.js | 11 +++++++++++ .../apps/maps/embeddable/embeddable_state.js | 7 +++++++ .../apps/maps/embeddable/save_and_return.js | 17 +++++++++++++++++ .../maps/embeddable/tooltip_filter_actions.js | 16 ++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 0c8a208e92ece..c5c02135ea976 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -16,9 +16,19 @@ export default function ({ getPageObjects, getService }) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); + const security = getService('security'); describe('embed in dashboard', () => { before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'geoshape_data_reader', + 'meta_for_geoshape_data_reader', + 'global_dashboard_read', + ], + false + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, @@ -31,6 +41,7 @@ export default function ({ getPageObjects, getService }) { await kibanaServer.uiSettings.replace({ [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: false, }); + await security.testUser.restoreDefaults(); }); async function getRequestTimestamp() { diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js index b5640eb4ec2ea..697f6cc251b13 100644 --- a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js @@ -9,11 +9,14 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['common', 'dashboard', 'maps']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const dashboardAddPanel = getService('dashboardAddPanel'); const DASHBOARD_NAME = 'verify_map_embeddable_state'; describe('embeddable state', () => { before(async () => { + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); @@ -26,6 +29,10 @@ export default function ({ getPageObjects, getService }) { await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_NAME); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should render map with center and zoom from embeddable state', async () => { const { lat, lon, zoom } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(0); diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js index 4aa44799db1f4..40af8ddb9d44b 100644 --- a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js +++ b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js @@ -12,8 +12,25 @@ export default function ({ getPageObjects, getService }) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardVisualizations = getService('dashboardVisualizations'); const testSubjects = getService('testSubjects'); + const security = getService('security'); describe('save and return work flow', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); describe('new map', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index d612a3776d211..f66104fc6a175 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -11,12 +11,24 @@ export default function ({ getPageObjects, getService }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); + const security = getService('security'); describe('tooltip filter actions', () => { + before(async () => { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + 'global_discover_read', + ]); + }); async function loadDashboardAndOpenTooltip() { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); + await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('dash for tooltip filter action test'); @@ -24,6 +36,10 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.lockTooltipAtPosition(200, -200); } + after(async () => { + await security.testUser.restoreDefaults(); + }); + describe('apply filter to current view', () => { before(async () => { await loadDashboardAndOpenTooltip(); From a38a9d3e0389fd7e79b8451876d1da51af360cb1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth <kaarina.tungseth@elastic.co> Date: Wed, 2 Dec 2020 09:39:26 -0600 Subject: [PATCH 060/107] [DOCS] Fix Creaxes Logstash pipeline API page (#84780) --- .../logstash-configuration-management/create-logstash.asciidoc | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/api/logstash-configuration-management/create-logstash.asciidoc b/docs/api/logstash-configuration-management/create-logstash.asciidoc index b608f4ee698f7..9bd5a9028ee9a 100644 --- a/docs/api/logstash-configuration-management/create-logstash.asciidoc +++ b/docs/api/logstash-configuration-management/create-logstash.asciidoc @@ -20,9 +20,6 @@ experimental[] Create a centrally-managed Logstash pipeline, or update an existi [[logstash-configuration-management-api-create-request-body]] ==== Request body -`id`:: - (Required, string) The pipeline ID. - `description`:: (Optional, string) The pipeline description. From c73de2677337e4054954c068744f8b29ab3ce29c Mon Sep 17 00:00:00 2001 From: Wylie Conlon <william.conlon@elastic.co> Date: Wed, 2 Dec 2020 11:16:59 -0500 Subject: [PATCH 061/107] [Lens] Refactor function selection invalid state (#84599) * [Lens] Refactor function selection invalid state * Fix types per review comment --- .../editor_frame/config_panel/layer_panel.tsx | 16 +- .../dimension_panel/dimension_editor.tsx | 175 +++++++++--------- .../dimension_panel/dimension_panel.test.tsx | 68 +++++-- .../dimension_panel/droppable.test.ts | 2 + .../dimension_panel/field_select.tsx | 71 +++---- .../indexpattern.test.ts | 40 ++++ .../indexpattern_datasource/indexpattern.tsx | 16 +- .../operations/__mocks__/index.ts | 1 + .../operations/definitions/column_types.ts | 3 +- .../operations/layer_helpers.test.ts | 46 +++-- .../operations/layer_helpers.ts | 109 +++++++++-- x-pack/plugins/lens/public/types.ts | 1 + 12 files changed, 376 insertions(+), 172 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 67c6068dd4d91..cc456e843bb68 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -332,7 +332,7 @@ export function LayerPanel( > <ColorIndicator accessorConfig={accessorConfig}> <NativeRenderer - render={props.datasourceMap[datasourceId].renderDimensionTrigger} + render={layerDatasource.renderDimensionTrigger} nativeProps={{ ...layerDatasourceConfigProps, columnId: accessor, @@ -464,12 +464,22 @@ export function LayerPanel( <DimensionContainer isOpen={!!activeId} groupLabel={activeGroup?.groupLabel || ''} - handleClose={() => setActiveDimension(initialActiveDimensionState)} + handleClose={() => { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + props.updateDatasource(datasourceId, newState); + } + setActiveDimension(initialActiveDimensionState); + }} panel={ <> {activeGroup && activeId && ( <NativeRenderer - render={props.datasourceMap[datasourceId].renderDimensionEditor} + render={layerDatasource.renderDimensionEditor} nativeProps={{ ...layerDatasourceConfigProps, core: props.core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 62de601bb7888..4c3def0e5bc7f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; -import { IndexPatternColumn, OperationType } from '../indexpattern'; +import { IndexPatternColumn } from '../indexpattern'; import { operationDefinitionMap, getOperationDisplay, @@ -26,6 +26,8 @@ import { replaceColumn, deleteColumn, updateColumnParam, + resetIncomplete, + FieldBasedIndexPatternColumn, } from '../operations'; import { mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -106,14 +108,14 @@ export function DimensionEditor(props: DimensionEditorProps) { hideGrouping, } = props; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; - const [ - incompatibleSelectedOperationType, - setInvalidOperationType, - ] = useState<OperationType | null>(null); const selectedOperationDefinition = selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + const incompleteInfo = (state.layers[layerId].incompleteColumns ?? {})[columnId]; + const incompleteOperation = incompleteInfo?.operationType; + const incompleteField = incompleteInfo?.sourceField ?? null; + const ParamEditor = selectedOperationDefinition?.paramEditor; const possibleOperations = useMemo(() => { @@ -138,7 +140,7 @@ export function DimensionEditor(props: DimensionEditorProps) { hasField(selectedColumn) && definition.input === 'field' && fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || - (selectedColumn && !hasField(selectedColumn) && definition.input !== 'field'), + (selectedColumn && !hasField(selectedColumn) && definition.input === 'none'), }; }); @@ -154,10 +156,8 @@ export function DimensionEditor(props: DimensionEditorProps) { const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map( ({ operationType, compatibleWithCurrentField }) => { const isActive = Boolean( - incompatibleSelectedOperationType === operationType || - (!incompatibleSelectedOperationType && - selectedColumn && - selectedColumn.operationType === operationType) + incompleteOperation === operationType || + (!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType) ); let color: EuiListGroupItemProps['color'] = 'primary'; @@ -184,9 +184,17 @@ export function DimensionEditor(props: DimensionEditorProps) { }`, onClick() { if (operationDefinitionMap[operationType].input === 'none') { - // Clear invalid state because we are creating a valid column - setInvalidOperationType(null); if (selectedColumn?.operationType === operationType) { + // Clear invalid state because we are reseting to a valid column + if (incompleteInfo) { + setState( + mergeLayer({ + state, + layerId, + newLayer: resetIncomplete(state.layers[layerId], columnId), + }) + ); + } return; } const newLayer = insertOrReplaceColumn({ @@ -216,15 +224,34 @@ export function DimensionEditor(props: DimensionEditorProps) { }) ); } else { - setInvalidOperationType(operationType); + setState( + mergeLayer({ + state, + layerId, + newLayer: insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: undefined, + }), + }) + ); } trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - setInvalidOperationType(null); - if (selectedColumn.operationType === operationType) { + if (incompleteInfo) { + setState( + mergeLayer({ + state, + layerId, + newLayer: resetIncomplete(state.layers[layerId], columnId), + }) + ); + } return; } @@ -268,18 +295,17 @@ export function DimensionEditor(props: DimensionEditorProps) { <div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--shaded"> {!selectedColumn || selectedOperationDefinition?.input === 'field' || - (incompatibleSelectedOperationType && - operationDefinitionMap[incompatibleSelectedOperationType].input === 'field') ? ( + (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') ? ( <EuiFormRow data-test-subj="indexPattern-field-selection-row" label={i18n.translate('xpack.lens.indexPattern.chooseField', { defaultMessage: 'Select a field', })} fullWidth - isInvalid={Boolean(incompatibleSelectedOperationType || currentFieldIsInvalid)} + isInvalid={Boolean(incompleteOperation || currentFieldIsInvalid)} error={getErrorMessage( selectedColumn, - Boolean(incompatibleSelectedOperationType), + Boolean(incompleteOperation), selectedOperationDefinition?.input, currentFieldIsInvalid )} @@ -289,11 +315,17 @@ export function DimensionEditor(props: DimensionEditorProps) { currentIndexPattern={currentIndexPattern} existingFields={state.existingFields} operationSupportMatrix={operationSupportMatrix} - selectedColumnOperationType={selectedColumn && selectedColumn.operationType} - selectedColumnSourceField={ - selectedColumn && hasField(selectedColumn) ? selectedColumn.sourceField : undefined + selectedOperationType={ + // Allows operation to be selected before creating a valid column + selectedColumn ? selectedColumn.operationType : incompleteOperation } - incompatibleSelectedOperationType={incompatibleSelectedOperationType} + selectedField={ + // Allows field to be selected + incompleteField + ? incompleteField + : (selectedColumn as FieldBasedIndexPatternColumn)?.sourceField + } + incompleteOperation={incompleteOperation} onDeleteColumn={() => { setState( mergeLayer({ @@ -304,53 +336,25 @@ export function DimensionEditor(props: DimensionEditorProps) { ); }} onChoose={(choice) => { - let newLayer: IndexPatternLayer; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // Replaces just the field - newLayer = replaceColumn({ - layer: state.layers[layerId], - columnId, - indexPattern: currentIndexPattern, - op: choice.operationType, - field: currentIndexPattern.getFieldByName(choice.field)!, - }); - } else { - // Finds a new operation - const compatibleOperations = - ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || - new Set(); - let operation; - if (compatibleOperations.size > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.has(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations.values().next().value; - } else if ('field' in choice) { - operation = choice.operationType; - } - newLayer = insertOrReplaceColumn({ - layer: state.layers[layerId], - columnId, - field: currentIndexPattern.getFieldByName(choice.field), - indexPattern: currentIndexPattern, - op: operation as OperationType, - }); - } - - setState(mergeLayer({ state, layerId, newLayer })); - setInvalidOperationType(null); + setState( + mergeLayer({ + state, + layerId, + newLayer: insertOrReplaceColumn({ + layer: state.layers[layerId], + columnId, + indexPattern: currentIndexPattern, + op: choice.operationType, + field: currentIndexPattern.getFieldByName(choice.field), + }), + }) + ); }} /> </EuiFormRow> ) : null} - {!currentFieldIsInvalid && !incompatibleSelectedOperationType && selectedColumn && ( + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( <TimeScaling selectedColumn={selectedColumn} columnId={columnId} @@ -361,33 +365,30 @@ export function DimensionEditor(props: DimensionEditorProps) { /> )} - {!currentFieldIsInvalid && - !incompatibleSelectedOperationType && - selectedColumn && - ParamEditor && ( - <> - <ParamEditor - state={state} - setState={setState} - columnId={columnId} - currentColumn={state.layers[layerId].columns[columnId]} - storage={props.storage} - uiSettings={props.uiSettings} - savedObjectsClient={props.savedObjectsClient} - layerId={layerId} - http={props.http} - dateRange={props.dateRange} - data={props.data} - /> - </> - )} + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ParamEditor && ( + <> + <ParamEditor + state={state} + setState={setState} + columnId={columnId} + currentColumn={state.layers[layerId].columns[columnId]} + storage={props.storage} + uiSettings={props.uiSettings} + savedObjectsClient={props.savedObjectsClient} + layerId={layerId} + http={props.http} + dateRange={props.dateRange} + data={props.data} + /> + </> + )} </div> <EuiSpacer size="s" /> {!currentFieldIsInvalid && ( <div className="lnsIndexPatternDimensionEditor__section"> - {!incompatibleSelectedOperationType && selectedColumn && ( + {!incompleteInfo && selectedColumn && ( <LabelInput value={selectedColumn.label} onChange={(value) => { @@ -411,7 +412,7 @@ export function DimensionEditor(props: DimensionEditorProps) { /> )} - {!incompatibleSelectedOperationType && !hideGrouping && ( + {!incompleteInfo && !hideGrouping && ( <BucketNestingEditor layer={state.layers[props.layerId]} columnId={props.columnId} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 29a0586c92ffe..d4197f9395660 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -142,6 +142,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { col1: { label: 'Date histogram of timestamp', + customLabel: true, dataType: 'date', isBucketed: true, @@ -153,11 +154,16 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'timestamp', }, }, + incompleteColumns: {}, }, }, }; - setState = jest.fn(); + setState = jest.fn().mockImplementation((newState) => { + if (wrapper instanceof ReactWrapper) { + wrapper.setProps({ state: newState }); + } + }); defaultProps = { state, @@ -544,7 +550,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { + it('should set the state if selecting an operation incompatible with the current field', () => { wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} />); act(() => { @@ -553,7 +559,20 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); }); - expect(setState).not.toHaveBeenCalled(); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, + }, + }, + }); }); it('should show error message in invalid state', () => { @@ -566,8 +585,6 @@ describe('IndexPatternDimensionEditorPanel', () => { expect( wrapper.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error') ).toBeDefined(); - - expect(setState).not.toHaveBeenCalled(); }); it('should leave error state if a compatible operation is selected', () => { @@ -664,6 +681,17 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} columnId={'col2'} />); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'avg' }, + }, + }, + }, + }); const comboBox = wrapper .find(EuiComboBox) @@ -675,7 +703,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![2]]); }); - expect(setState).toHaveBeenCalledWith({ + expect(setState).toHaveBeenLastCalledWith({ ...state, layers: { first: { @@ -759,11 +787,9 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should set datasource state if compatible field is selected for operation', () => { wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} />); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); const comboBox = wrapper .find(EuiComboBox) @@ -774,7 +800,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith({ + expect(setState).toHaveBeenLastCalledWith({ ...state, layers: { first: { @@ -1046,6 +1072,20 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { + operationType: 'avg', + }, + }, + }, + }, + }); + const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]'); @@ -1212,6 +1252,9 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should add a column on selection of a field', () => { + // Prevents field format from being loaded + setState.mockImplementation(() => {}); + wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} columnId={'col2'} />); const comboBox = wrapper @@ -1231,6 +1274,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ + operationType: 'range', sourceField: 'bytes', // Other parts of this don't matter for this test }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 48240a5417108..48d8d55114115 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -106,6 +106,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { col1: { label: 'Date histogram of timestamp', + customLabel: true, dataType: 'date', isBucketed: true, @@ -117,6 +118,7 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'timestamp', }, }, + incompleteColumns: {}, }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 9bc3e52822cf4..135c5dcf37db9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -28,14 +28,14 @@ import { fieldExists } from '../pure_helpers'; export interface FieldChoice { type: 'field'; field: string; - operationType?: OperationType; + operationType: OperationType; } export interface FieldSelectProps extends EuiComboBoxProps<{}> { currentIndexPattern: IndexPattern; - incompatibleSelectedOperationType: OperationType | null; - selectedColumnOperationType?: OperationType; - selectedColumnSourceField?: string; + selectedOperationType?: OperationType; + selectedField?: string; + incompleteOperation?: OperationType; operationSupportMatrix: OperationSupportMatrix; onChoose: (choice: FieldChoice) => void; onDeleteColumn: () => void; @@ -45,9 +45,9 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { export function FieldSelect({ currentIndexPattern, - incompatibleSelectedOperationType, - selectedColumnOperationType, - selectedColumnSourceField, + incompleteOperation, + selectedOperationType, + selectedField, operationSupportMatrix, onChoose, onDeleteColumn, @@ -59,14 +59,10 @@ export function FieldSelect({ const memoizedFieldOptions = useMemo(() => { const fields = Object.keys(operationByField).sort(); + const currentOperationType = incompleteOperation ?? selectedOperationType; + function isCompatibleWithCurrentOperation(fieldName: string) { - if (incompatibleSelectedOperationType) { - return operationByField[fieldName]!.has(incompatibleSelectedOperationType); - } - return ( - !selectedColumnOperationType || - operationByField[fieldName]!.has(selectedColumnOperationType) - ); + return !currentOperationType || operationByField[fieldName]!.has(currentOperationType); } const [specialFields, normalFields] = _.partition( @@ -81,20 +77,25 @@ export function FieldSelect({ function fieldNamesToOptions(items: string[]) { return items .filter((field) => currentIndexPattern.getFieldByName(field)?.displayName) - .map((field) => ({ - label: currentIndexPattern.getFieldByName(field)?.displayName, - value: { - type: 'field', - field, - dataType: currentIndexPattern.getFieldByName(field)?.type, - operationType: - selectedColumnOperationType && isCompatibleWithCurrentOperation(field) - ? selectedColumnOperationType - : undefined, - }, - exists: containsData(field), - compatible: isCompatibleWithCurrentOperation(field), - })) + .map((field) => { + return { + label: currentIndexPattern.getFieldByName(field)?.displayName, + value: { + type: 'field', + field, + dataType: currentIndexPattern.getFieldByName(field)?.type, + // Use the operation directly, or choose the first compatible operation. + // All fields are guaranteed to have at least one operation because they + // won't appear in the list otherwise + operationType: + currentOperationType && isCompatibleWithCurrentOperation(field) + ? currentOperationType + : operationByField[field]!.values().next().value, + }, + exists: containsData(field), + compatible: isCompatibleWithCurrentOperation(field), + }; + }) .sort((a, b) => { if (a.compatible && !b.compatible) { return -1; @@ -157,8 +158,8 @@ export function FieldSelect({ metaFieldsOptions, ].filter(Boolean); }, [ - incompatibleSelectedOperationType, - selectedColumnOperationType, + incompleteOperation, + selectedOperationType, currentIndexPattern, operationByField, existingFields, @@ -174,15 +175,15 @@ export function FieldSelect({ defaultMessage: 'Field', })} options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]} - isInvalid={Boolean(incompatibleSelectedOperationType || fieldIsInvalid)} + isInvalid={Boolean(incompleteOperation || fieldIsInvalid)} selectedOptions={ - ((selectedColumnOperationType && selectedColumnSourceField + ((selectedOperationType && selectedField ? [ { label: fieldIsInvalid - ? selectedColumnSourceField - : currentIndexPattern.getFieldByName(selectedColumnSourceField)?.displayName, - value: { type: 'field', field: selectedColumnSourceField }, + ? selectedField + : currentIndexPattern.getFieldByName(selectedField)?.displayName, + value: { type: 'field', field: selectedField }, }, ] : []) as unknown) as EuiComboBoxOptionOption[] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 5153a74409bee..3c1c8d5f2c006 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -988,4 +988,44 @@ describe('IndexPattern Data Source', () => { expect(getErrorMessages).toHaveBeenCalledTimes(1); }); }); + + describe('#updateStateOnCloseDimension', () => { + it('should clear the incomplete column', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { + col1: { operationType: 'avg' as const }, + col2: { operationType: 'sum' as const }, + }, + }, + }, + currentIndexPatternId: '1', + }; + expect( + indexPatternDatasource.updateStateOnCloseDimension!({ + state, + layerId: 'first', + columnId: 'col1', + }) + ).toEqual({ + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { col2: { operationType: 'sum' } }, + }, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 289b6bbe3f25b..948619c6b07e5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -46,7 +46,7 @@ import { normalizeOperationDataType, } from './utils'; import { LayerPanel } from './layerpanel'; -import { IndexPatternColumn, getErrorMessages } from './operations'; +import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -319,6 +319,20 @@ export function getIndexPatternDatasource({ canHandleDrop, onDrop, + // Reset the temporary invalid state when closing the editor + updateStateOnCloseDimension: ({ state, layerId, columnId }) => { + const layer = { ...state.layers[layerId] }; + const newIncomplete: Record<string, IncompleteColumn> = { + ...(state.layers[layerId].incompleteColumns || {}), + }; + delete newIncomplete[columnId]; + return mergeLayer({ + state, + layerId, + newLayer: { ...layer, incompleteColumns: newIncomplete }, + }); + }, + getPublicAPI({ state, layerId }: PublicAPIProps<IndexPatternPrivateState>) { const columnLabelMap = indexPatternDatasource.uniqueLabels(state); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 385f2ab941ef2..077255b75a769 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -40,6 +40,7 @@ export const { isColumnTransferable, getErrorMessages, isReferenced, + resetIncomplete, } = actualHelpers; export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index d0a0fb4b28588..de3f158cca620 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -6,6 +6,7 @@ import type { Operation } from '../../../types'; import { TimeScaleUnit } from '../../time_scale'; +import type { OperationType } from '../definitions'; export interface BaseIndexPatternColumn extends Operation { // Private @@ -39,6 +40,6 @@ export interface ReferenceBasedIndexPatternColumn // Used to store the temporary invalid state export interface IncompleteColumn { - operationType?: string; + operationType?: OperationType; sourceField?: string; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 0d103a766c23a..7ffbeac39c6f5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -228,16 +228,22 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); - it('should throw if the aggregation does not support the field', () => { - expect(() => { + it('should insert both incomplete states if the aggregation does not support the field', () => { + expect( insertNewColumn({ layer: { indexPatternId: '1', columnOrder: [], columns: {} }, columnId: 'col1', indexPattern, op: 'terms', field: indexPattern.fields[0], - }); - }).toThrow(); + }) + ).toEqual( + expect.objectContaining({ + incompleteColumns: { + col1: { operationType: 'terms', sourceField: 'timestamp' }, + }, + }) + ); }); it('should put the terms agg ahead of the date histogram', () => { @@ -531,8 +537,8 @@ describe('state_helpers', () => { }).toThrow(); }); - it('should throw if switching to a field-based operation without providing a field', () => { - expect(() => { + it('should set incompleteColumns when switching to a field-based operation without providing a field', () => { + expect( replaceColumn({ layer: { indexPatternId: '1', @@ -554,12 +560,19 @@ describe('state_helpers', () => { }, columnId: 'col1', indexPattern, - op: 'date_histogram', - }); - }).toThrow(); + op: 'terms', + }) + ).toEqual( + expect.objectContaining({ + columns: { col1: expect.objectContaining({ operationType: 'date_histogram' }) }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, + }) + ); }); - it('should carry over params from old column if the switching fields', () => { + it('should carry over params from old column if switching fields', () => { expect( replaceColumn({ layer: { @@ -592,7 +605,7 @@ describe('state_helpers', () => { ); }); - it('should transition from field-based to fieldless operation', () => { + it('should transition from field-based to fieldless operation, clearing incomplete', () => { expect( replaceColumn({ layer: { @@ -612,14 +625,20 @@ describe('state_helpers', () => { }, }, }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, }, indexPattern, columnId: 'col1', op: 'filters', - }).columns.col1 + }) ).toEqual( expect.objectContaining({ - operationType: 'filters', + columns: { + col1: expect.objectContaining({ operationType: 'filters' }), + }, + incompleteColumns: {}, }) ); }); @@ -944,6 +963,7 @@ describe('state_helpers', () => { isTransferable: jest.fn(), toExpression: jest.fn().mockReturnValue([]), getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: () => 'Test reference', }; const layer: IndexPatternLayer = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 260ed180da921..b16418d44ba33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -153,15 +153,50 @@ export function insertNewColumn({ } } - if (!field) { - throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + const invalidFieldName = (layer.incompleteColumns ?? {})[columnId]?.sourceField; + const invalidField = invalidFieldName ? indexPattern.getFieldByName(invalidFieldName) : undefined; + + if (!field && invalidField) { + const possibleOperation = operationDefinition.getPossibleOperationForField(invalidField); + if (!possibleOperation) { + throw new Error( + `Tried to create an invalid operation ${operationDefinition.type} using previously selected field ${invalidField.name}` + ); + } + const isBucketed = Boolean(possibleOperation.isBucketed); + if (isBucketed) { + return addBucket( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ); + } else { + return addMetric( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ); + } + } else if (!field) { + // Labels don't need to be updated because it's incomplete + return { + ...layer, + incompleteColumns: { + ...(layer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; } const possibleOperation = operationDefinition.getPossibleOperationForField(field); if (!possibleOperation) { - throw new Error( - `Tried to create an invalid operation ${operationDefinition.type} on ${field.name}` - ); + return { + ...layer, + incompleteColumns: { + ...(layer.incompleteColumns ?? {}), + [columnId]: { operationType: op, sourceField: field.name }, + }, + }; } const isBucketed = Boolean(possibleOperation.isBucketed); if (isBucketed) { @@ -208,6 +243,8 @@ export function replaceColumn({ if (isNewOperation) { let tempLayer = { ...layer }; + tempLayer = resetIncomplete(tempLayer, columnId); + if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { tempLayer = deleteColumn({ layer: tempLayer, columnId: id }); @@ -217,8 +254,6 @@ export function replaceColumn({ if (operationDefinition.input === 'fullReference') { const referenceIds = operationDefinition.requiredReferences.map(() => generateId()); - const incompleteColumns = { ...(tempLayer.incompleteColumns || {}) }; - delete incompleteColumns[columnId]; const newColumns = { ...tempLayer.columns, [columnId]: operationDefinition.buildColumn({ @@ -232,7 +267,6 @@ export function replaceColumn({ ...tempLayer, columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), columns: newColumns, - incompleteColumns, }; } @@ -249,7 +283,13 @@ export function replaceColumn({ } if (!field) { - throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + return { + ...tempLayer, + incompleteColumns: { + ...(tempLayer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; } let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); @@ -296,7 +336,7 @@ function addBucket( column: IndexPatternColumn, addedColumnId: string ): IndexPatternLayer { - const [buckets, metrics] = separateBucketColumns(layer); + const [buckets, metrics, references] = getExistingColumnGroups(layer); const oldDateHistogramIndex = layer.columnOrder.findIndex( (columnId) => layer.columns[columnId].operationType === 'date_histogram' @@ -310,17 +350,19 @@ function addBucket( addedColumnId, ...buckets.slice(oldDateHistogramIndex, buckets.length), ...metrics, + ...references, ]; } else { // Insert the new bucket after existing buckets. Users will see the same data // they already had, with an extra level of detail. - updatedColumnOrder = [...buckets, addedColumnId, ...metrics]; + updatedColumnOrder = [...buckets, addedColumnId, ...metrics, ...references]; } - return { - ...layer, + const tempLayer = { + ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column }, columnOrder: updatedColumnOrder, }; + return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } function addMetric( @@ -328,18 +370,15 @@ function addMetric( column: IndexPatternColumn, addedColumnId: string ): IndexPatternLayer { - return { - ...layer, + const tempLayer = { + ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column, }, columnOrder: [...layer.columnOrder, addedColumnId], }; -} - -function separateBucketColumns(layer: IndexPatternLayer) { - return partition(layer.columnOrder, (columnId) => layer.columns[columnId]?.isBucketed); + return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } export function getMetricOperationTypes(field: IndexPatternField) { @@ -442,9 +481,24 @@ export function deleteColumn({ return { ...newLayer, columnOrder: getColumnOrder(newLayer), incompleteColumns: newIncomplete }; } +// Derives column order from column object, respects existing columnOrder +// when possible, but also allows new columns to be added to the order export function getColumnOrder(layer: IndexPatternLayer): string[] { + const entries = Object.entries(layer.columns); + entries.sort(([idA], [idB]) => { + const indexA = layer.columnOrder.indexOf(idA); + const indexB = layer.columnOrder.indexOf(idB); + if (indexA > -1 && indexB > -1) { + return indexA - indexB; + } else if (indexA > -1) { + return -1; + } else { + return 1; + } + }); + const [direct, referenceBased] = _.partition( - Object.entries(layer.columns), + entries, ([id, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference' ); // If a reference has another reference as input, put it last in sort order @@ -465,6 +519,15 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { .concat(referenceBased.map(([id]) => id)); } +// Splits existing columnOrder into the three categories +function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] { + const [direct, referenced] = partition( + layer.columnOrder, + (columnId) => layer.columns[columnId] && !('references' in layer.columns[columnId]) + ); + return [...partition(direct, (columnId) => layer.columns[columnId]?.isBucketed), referenced]; +} + /** * Returns true if the given column can be applied to the given index pattern */ @@ -601,3 +664,9 @@ function isOperationAllowedAsReference({ hasValidMetadata ); } + +export function resetIncomplete(layer: IndexPatternLayer, columnId: string): IndexPatternLayer { + const incompleteColumns = { ...(layer.incompleteColumns ?? {}) }; + delete incompleteColumns[columnId]; + return { ...layer, incompleteColumns }; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 65c565087af72..2b3cd02c09d1e 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -167,6 +167,7 @@ export interface Datasource<T = unknown, P = unknown> { renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps<T>) => void; canHandleDrop: (props: DatasourceDimensionDropProps<T>) => boolean; onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => false | true | { deleted: string }; + updateStateOnCloseDimension?: (props: { layerId: string; columnId: string; state: T }) => T; toExpression: (state: T, layerId: string) => Ast | string | null; From d990b27ab202a9cd7a3164833ca7fe417a0a8538 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov <restrry@gmail.com> Date: Wed, 2 Dec 2020 19:28:46 +0300 Subject: [PATCH 062/107] bump es-js version (#84770) --- package.json | 2 +- yarn.lock | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 7028b3093dc4f..d5c1f247d87d7 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@babel/core": "^7.11.6", "@babel/runtime": "^7.11.2", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "7.10.0-rc.1", + "@elastic/elasticsearch": "7.10.0", "@elastic/ems-client": "7.11.0", "@elastic/eui": "30.2.0", "@elastic/filesaver": "1.1.2", diff --git a/yarn.lock b/yarn.lock index 8644a53e6f52f..28f032ef7122f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1404,13 +1404,12 @@ version "0.0.0" uid "" -"@elastic/elasticsearch@7.10.0-rc.1": - version "7.10.0-rc.1" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0-rc.1.tgz#c23fc5cbfdb40cf2ce6f9cd796b75940e8c9dc8a" - integrity sha512-STaBlEwYbT8yT3HJ+mbO1kx+Kb7Ft7Q0xG5GxZbqbAJ7PZvgGgJWwN7jUg4oKJHbTfxV3lPvFa+PaUK2TqGuYg== +"@elastic/elasticsearch@7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0.tgz#da105a9c1f14146f9f2cab4e7026cb7949121b8d" + integrity sha512-vXtMAQf5/DwqeryQgRriMtnFppJNLc/R7/R0D8E+wG5/kGM5i7mg+Hi7TM4NZEuXgtzZ2a/Nf7aR0vLyrxOK/w== dependencies: debug "^4.1.1" - decompress-response "^4.2.0" hpagent "^0.1.1" ms "^2.1.1" pump "^3.0.0" @@ -11187,13 +11186,6 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -decompress-response@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.0.tgz#805ca9d1d3cdf17a03951475ad6cdc93115cec3f" - integrity sha512-MHebOkORCgLW1ramLri5vzfR4r7HgXXrVkVr/eaPVRCtYWFUp9hNAuqsBxhpABbpqd7zY2IrjxXfTuaVrW0Z2A== - dependencies: - mimic-response "^2.0.0" - decompress-response@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" From cbc61afcce023d0478784c953d092e4b8dd9a375 Mon Sep 17 00:00:00 2001 From: ymao1 <ying.mao@elastic.co> Date: Wed, 2 Dec 2020 11:49:24 -0500 Subject: [PATCH 063/107] [Task Manager] Skip removed task types when claiming tasks (#84273) * Checking if task type is in registered list * Loading esArchiver data with removed task type for testing * PR fixes --- .../mark_available_tasks_as_claimed.test.ts | 16 +- .../mark_available_tasks_as_claimed.ts | 16 +- x-pack/plugins/task_manager/server/task.ts | 1 + .../task_manager/server/task_store.test.ts | 32 ++- .../plugins/task_manager/server/task_store.ts | 2 + .../server/task_type_dictionary.ts | 8 +- .../task_manager_removed_types/data.json | 30 +++ .../task_manager_removed_types/mappings.json | 225 ++++++++++++++++++ .../test_suites/task_manager/index.ts | 1 + .../task_management_removed_types.ts | 101 ++++++++ 10 files changed, 408 insertions(+), 24 deletions(-) create mode 100644 x-pack/test/functional/es_archives/task_manager_removed_types/data.json create mode 100644 x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json create mode 100644 x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 8a94ae4ed82f5..8b0a8323d9452 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -50,6 +50,7 @@ describe('mark_available_tasks_as_claimed', () => { update: updateFieldsAndMarkAsFailed( fieldUpdates, claimTasksById || [], + definitions.getAllTypes(), Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; }, {}) @@ -114,12 +115,16 @@ if (doc['task.runAt'].size()!=0) { seq_no_primary_term: true, script: { source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', @@ -129,6 +134,7 @@ if (doc['task.runAt'].size()!=0) { retryAt: claimOwnershipUntil, }, claimTasksById: [], + registeredTaskTypes: ['sampleTask', 'otherTask'], taskMaxAttempts: { sampleTask: 5, otherTask: 1, diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 072ec4648201a..ecd8107eef915 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -105,21 +105,27 @@ export const updateFieldsAndMarkAsFailed = ( [field: string]: string | number | Date; }, claimTasksById: string[], + registeredTaskTypes: string[], taskMaxAttempts: { [field: string]: number } ): ScriptClause => ({ source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', params: { fieldUpdates, claimTasksById, + registeredTaskTypes, taskMaxAttempts, }, }); diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 8b7870550040f..e832a95ac3caa 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -166,6 +166,7 @@ export enum TaskStatus { Claiming = 'claiming', Running = 'running', Failed = 'failed', + Unrecognized = 'unrecognized', } export enum TaskLifecycleResult { diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 46e55df4ee1e6..81d72c68b3a9e 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -578,12 +578,16 @@ if (doc['task.runAt'].size()!=0) { expect(script).toMatchObject({ source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', @@ -593,6 +597,7 @@ if (doc['task.runAt'].size()!=0) { 'task:33c6977a-ed6d-43bd-98d9-3f827f7b7cd8', 'task:a208b22c-14ec-4fb4-995f-d2ff7a3b03b8', ], + registeredTaskTypes: ['foo', 'bar'], taskMaxAttempts: { bar: customMaxAttempts, foo: maxAttempts, @@ -644,18 +649,23 @@ if (doc['task.runAt'].size()!=0) { }); expect(script).toMatchObject({ source: ` - if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { - ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) - .join(' ')} + if (params.registeredTaskTypes.contains(ctx._source.task.taskType)) { + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } } else { - ctx._source.task.status = "failed"; + ctx._source.task.status = "unrecognized"; } `, lang: 'painless', params: { fieldUpdates, claimTasksById: [], + registeredTaskTypes: ['report', 'dernstraight', 'yawn'], taskMaxAttempts: { dernstraight: 2, report: 2, @@ -1218,7 +1228,7 @@ if (doc['task.runAt'].size()!=0) { describe('getLifecycle', () => { test('returns the task status if the task exists ', async () => { - expect.assertions(4); + expect.assertions(5); return Promise.all( Object.values(TaskStatus).map(async (status) => { const task = { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 04ee3529bcc0b..0d5d2431e227f 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -260,6 +260,7 @@ export class TaskStore { claimTasksById: OwnershipClaimingOpts['claimTasksById'], size: OwnershipClaimingOpts['size'] ): Promise<number> { + const registeredTaskTypes = this.definitions.getAllTypes(); const taskMaxAttempts = [...this.definitions].reduce((accumulator, [type, { maxAttempts }]) => { return { ...accumulator, [type]: maxAttempts || this.maxAttempts }; }, {}); @@ -297,6 +298,7 @@ export class TaskStore { retryAt: claimOwnershipUntil, }, claimTasksById || [], + registeredTaskTypes, taskMaxAttempts ), sort, diff --git a/x-pack/plugins/task_manager/server/task_type_dictionary.ts b/x-pack/plugins/task_manager/server/task_type_dictionary.ts index cb7cda6dfa845..451b5dd7cad52 100644 --- a/x-pack/plugins/task_manager/server/task_type_dictionary.ts +++ b/x-pack/plugins/task_manager/server/task_type_dictionary.ts @@ -32,6 +32,10 @@ export class TaskTypeDictionary { return this.definitions.entries(); } + public getAllTypes() { + return [...this.definitions.keys()]; + } + public has(type: string) { return this.definitions.has(type); } @@ -44,9 +48,7 @@ export class TaskTypeDictionary { public ensureHas(type: string) { if (!this.has(type)) { throw new Error( - `Unsupported task type "${type}". Supported types are ${[...this.definitions.keys()].join( - ', ' - )}` + `Unsupported task type "${type}". Supported types are ${this.getAllTypes().join(', ')}` ); } } diff --git a/x-pack/test/functional/es_archives/task_manager_removed_types/data.json b/x-pack/test/functional/es_archives/task_manager_removed_types/data.json new file mode 100644 index 0000000000000..8594e9d567b8a --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_removed_types/data.json @@ -0,0 +1,30 @@ +{ + "type": "doc", + "value": { + "id": "task:be7e1250-3322-11eb-94c1-db6995e83f6a", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "params": "{\"originalParams\":{},\"superFly\":\"My middleware param!\"}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "sampleTaskRemovedType" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json b/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json new file mode 100644 index 0000000000000..c3a10064e905e --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json @@ -0,0 +1,225 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "0359d7fcc04da9878ee9aadbda38ba55", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "background-session": "721df406dbb7e35ac22e4df6c3ad2b2a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "task": "235412e52d09e7165fac8a67a43ad6b4", + "type": "2f4316de49999235636386fe51dc06c1", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0" + } + }, + "dynamic": "strict", + "properties": { + "migrationVersion": { + "dynamic": "true", + "properties": { + "task": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index b542bff3a4aa9..c1e7aad8ac36f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup2'); loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); + loadTestFile(require.resolve('./task_management_removed_types')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts new file mode 100644 index 0000000000000..a0ca970bac844 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import expect from '@kbn/expect'; +import url from 'url'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; + +export interface RawDoc { + _id: string; + _source: any; + _type?: string; +} +export interface SearchResults { + hits: { + hits: RawDoc[]; + }; +} + +type DeprecatedConcreteTaskInstance = Omit<ConcreteTaskInstance, 'schedule'> & { + interval: string; +}; + +type SerializedConcreteTaskInstance<State = string, Params = string> = Omit< + ConcreteTaskInstance, + 'state' | 'params' | 'scheduledAt' | 'startedAt' | 'retryAt' | 'runAt' +> & { + state: State; + params: Params; + scheduledAt: string; + startedAt: string | null; + retryAt: string | null; + runAt: string; +}; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const config = getService('config'); + const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); + + const REMOVED_TASK_TYPE_ID = 'be7e1250-3322-11eb-94c1-db6995e83f6a'; + + describe('removed task types', () => { + before(async () => { + await esArchiver.load('task_manager_removed_types'); + }); + + after(async () => { + await esArchiver.unload('task_manager_removed_types'); + }); + + function scheduleTask( + task: Partial<ConcreteTaskInstance | DeprecatedConcreteTaskInstance> + ): Promise<SerializedConcreteTaskInstance> { + return supertest + .post('/api/sample_tasks/schedule') + .set('kbn-xsrf', 'xxx') + .send({ task }) + .expect(200) + .then((response: { body: SerializedConcreteTaskInstance }) => response.body); + } + + function currentTasks<State = unknown, Params = unknown>(): Promise<{ + docs: Array<SerializedConcreteTaskInstance<State, Params>>; + }> { + return supertest + .get('/api/sample_tasks') + .expect(200) + .then((response) => response.body); + } + + it('should successfully schedule registered tasks and mark unregistered tasks as unrecognized', async () => { + const scheduledTask = await scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: `1s` }, + params: {}, + }); + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + expect(tasks.length).to.eql(2); + + const taskIds = tasks.map((task) => task.id); + expect(taskIds).to.contain(scheduledTask.id); + expect(taskIds).to.contain(REMOVED_TASK_TYPE_ID); + + const scheduledTaskInstance = tasks.find((task) => task.id === scheduledTask.id); + const removedTaskInstance = tasks.find((task) => task.id === REMOVED_TASK_TYPE_ID); + + expect(scheduledTaskInstance?.status).to.eql('claiming'); + expect(removedTaskInstance?.status).to.eql('unrecognized'); + }); + }); + }); +} From a20709cce66104f8a8651d07d2f78742713172c9 Mon Sep 17 00:00:00 2001 From: Larry Gregory <larry.gregory@elastic.co> Date: Wed, 2 Dec 2020 12:00:14 -0500 Subject: [PATCH 064/107] Deprecate disabling the spaces plugin (#83984) Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> --- docs/developer/plugin-list.asciidoc | 4 +- docs/settings/spaces-settings.asciidoc | 1 + x-pack/plugins/spaces/README.md | 10 ++++ x-pack/plugins/spaces/server/config.test.ts | 63 +++++++++++++++++++++ x-pack/plugins/spaces/server/config.ts | 19 ++++++- x-pack/plugins/spaces/server/index.ts | 9 ++- 6 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/spaces/README.md create mode 100644 x-pack/plugins/spaces/server/config.test.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5ee7131610584..e515abee6014c 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -507,8 +507,8 @@ Kibana. |or -|{kib-repo}blob/{branch}/x-pack/plugins/spaces[spaces] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/spaces/README.md[spaces] +|See Configuring Kibana Spaces. |{kib-repo}blob/{branch}/x-pack/plugins/stack_alerts/README.md[stackAlerts] diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index bda5f00f762cd..3b643f76f0c09 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -16,6 +16,7 @@ roles when Security is enabled. |=== | `xpack.spaces.enabled` | Set to `true` (default) to enable Spaces in {kib}. + This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. | `xpack.spaces.maxSpaces` | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations diff --git a/x-pack/plugins/spaces/README.md b/x-pack/plugins/spaces/README.md new file mode 100644 index 0000000000000..89194253dce4a --- /dev/null +++ b/x-pack/plugins/spaces/README.md @@ -0,0 +1,10 @@ +# Kibana Spaces Plugin + +See [Configuring Kibana Spaces](https://www.elastic.co/guide/en/kibana/current/spaces-settings-kb.html). + +The spaces plugin enables Kibana Spaces, which provide isolation and organization +for saved objects. + +Spaces also allow for a creating a curated Kibana experience, by hiding features that aren't relevant to your users. + +Spaces can be combined with Security to further enhance the authorization model. diff --git a/x-pack/plugins/spaces/server/config.test.ts b/x-pack/plugins/spaces/server/config.test.ts new file mode 100644 index 0000000000000..d27498eb6e3fc --- /dev/null +++ b/x-pack/plugins/spaces/server/config.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; +import { deepFreeze } from '@kbn/std'; +import { spacesConfigDeprecationProvider } from './config'; + +const applyConfigDeprecations = (settings: Record<string, any> = {}) => { + const deprecations = spacesConfigDeprecationProvider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map((deprecation) => ({ + deprecation, + path: '', + })), + (msg) => deprecationMessages.push(msg) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('spaces config', () => { + describe('deprecations', () => { + describe('enabled', () => { + it('logs a warning if xpack.spaces.enabled is set to false', () => { + const originalConfig = deepFreeze({ xpack: { spaces: { enabled: false } } }); + + const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); + + expect(messages).toMatchInlineSnapshot(` + Array [ + "Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)", + ] + `); + expect(migrated).toEqual(originalConfig); + }); + + it('does not log a warning if no settings are explicitly set', () => { + const originalConfig = deepFreeze({}); + + const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); + + expect(messages).toMatchInlineSnapshot(`Array []`); + expect(migrated).toEqual(originalConfig); + }); + + it('does not log a warning if xpack.spaces.enabled is set to true', () => { + const originalConfig = deepFreeze({ xpack: { spaces: { enabled: true } } }); + + const { messages, migrated } = applyConfigDeprecations({ ...originalConfig }); + + expect(messages).toMatchInlineSnapshot(`Array []`); + expect(migrated).toEqual(originalConfig); + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index a28624fb82c15..671b725ac1092 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -5,7 +5,11 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import type { + PluginInitializerContext, + ConfigDeprecationProvider, + ConfigDeprecation, +} from 'src/core/server'; import { Observable } from 'rxjs'; export const ConfigSchema = schema.object({ @@ -17,6 +21,19 @@ export function createConfig$(context: PluginInitializerContext) { return context.config.create<TypeOf<typeof ConfigSchema>>(); } +const disabledDeprecation: ConfigDeprecation = (config, fromPath, log) => { + if (config.xpack?.spaces?.enabled === false) { + log( + `Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)` + ); + } + return config; +}; + +export const spacesConfigDeprecationProvider: ConfigDeprecationProvider = () => { + return [disabledDeprecation]; +}; + export type ConfigType = ReturnType<typeof createConfig$> extends Observable<infer P> ? P : ReturnType<typeof createConfig$>; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 85f1facf6131c..4d3d184ec41a3 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; -import { ConfigSchema } from './config'; +import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; +import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; import { Plugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported @@ -22,6 +22,9 @@ export { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; export { ISpacesClient } from './spaces_client'; export { Space } from '../common/model/space'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: spacesConfigDeprecationProvider, +}; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); From b96b965387898af3480f49860665c5a53d7dc906 Mon Sep 17 00:00:00 2001 From: Spencer <email@spalger.com> Date: Wed, 2 Dec 2020 10:06:33 -0700 Subject: [PATCH 065/107] [build/node] log url when downloading node shasum info (#84692) Co-authored-by: spalger <spalger@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/build/tasks/nodejs/download_node_builds_task.ts | 2 +- src/dev/build/tasks/nodejs/node_shasums.test.ts | 3 ++- src/dev/build/tasks/nodejs/node_shasums.ts | 5 ++++- .../tasks/nodejs/verify_existing_node_builds_task.test.ts | 2 ++ .../build/tasks/nodejs/verify_existing_node_builds_task.ts | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.ts index ad42ea11436f5..93ad599e41e40 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.ts @@ -25,7 +25,7 @@ export const DownloadNodeBuilds: GlobalTask = { global: true, description: 'Downloading node.js builds for all platforms', async run(config, log) { - const shasums = await getNodeShasums(config.getNodeVersion()); + const shasums = await getNodeShasums(log, config.getNodeVersion()); await Promise.all( config.getNodePlatforms().map(async (platform) => { const { url, downloadPath, downloadName } = getNodeDownloadInfo(config, platform); diff --git a/src/dev/build/tasks/nodejs/node_shasums.test.ts b/src/dev/build/tasks/nodejs/node_shasums.test.ts index 08ac823c7ebf0..53d073afd6499 100644 --- a/src/dev/build/tasks/nodejs/node_shasums.test.ts +++ b/src/dev/build/tasks/nodejs/node_shasums.test.ts @@ -70,11 +70,12 @@ jest.mock('axios', () => ({ }, })); +import { ToolingLog } from '@kbn/dev-utils'; import { getNodeShasums } from './node_shasums'; describe('src/dev/build/tasks/nodejs/node_shasums', () => { it('resolves to an object with shasums for node downloads for version', async () => { - const shasums = await getNodeShasums('8.9.4'); + const shasums = await getNodeShasums(new ToolingLog(), '8.9.4'); expect(shasums).toEqual( expect.objectContaining({ 'node-v8.9.4.tar.gz': '729b44b32b2f82ecd5befac4f7518de0c4e3add34e8fe878f745740a66cbbc01', diff --git a/src/dev/build/tasks/nodejs/node_shasums.ts b/src/dev/build/tasks/nodejs/node_shasums.ts index e0926aa3e49e4..0f506dff4fd88 100644 --- a/src/dev/build/tasks/nodejs/node_shasums.ts +++ b/src/dev/build/tasks/nodejs/node_shasums.ts @@ -18,10 +18,13 @@ */ import axios from 'axios'; +import { ToolingLog } from '@kbn/dev-utils'; -export async function getNodeShasums(nodeVersion: string) { +export async function getNodeShasums(log: ToolingLog, nodeVersion: string) { const url = `https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v${nodeVersion}/SHASUMS256.txt`; + log.debug('Downloading shasum values for node version', nodeVersion, 'from', url); + const { status, data } = await axios.get(url); if (status !== 200) { diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index 9b03dcd828cf9..5b3c1bad74930 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -45,6 +45,7 @@ const testWriter = new ToolingLogCollectingWriter(); log.setWriters([testWriter]); expect.addSnapshotSerializer(createAnyInstanceSerializer(Config)); +expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); const nodeVersion = Fs.readFileSync(Path.resolve(REPO_ROOT, '.node-version'), 'utf8').trim(); expect.addSnapshotSerializer( @@ -100,6 +101,7 @@ it('checks shasums for each downloaded node build', async () => { [MockFunction] { "calls": Array [ Array [ + <ToolingLog>, "<node version>", ], ], diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts index 9ce0778d2d1f0..50684d866cbf5 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts @@ -25,7 +25,7 @@ export const VerifyExistingNodeBuilds: GlobalTask = { global: true, description: 'Verifying previously downloaded node.js build for all platforms', async run(config, log) { - const shasums = await getNodeShasums(config.getNodeVersion()); + const shasums = await getNodeShasums(log, config.getNodeVersion()); await Promise.all( config.getNodePlatforms().map(async (platform) => { From 7f969136a3f3a7b85eb2dcaef7b886953a179a1c Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala <bohdan.tsymbala@elastic.co> Date: Wed, 2 Dec 2020 18:55:08 +0100 Subject: [PATCH 066/107] Added migration of policy for AV registration config. (#84779) * Added migration of policy for AV registration config. * Updated migration a bit to be more safe. --- .../common/endpoint/policy/migrations/to_v7_11.0.test.ts | 3 ++- .../common/endpoint/policy/migrations/to_v7_11.0.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts index 6d3e320fba3af..b516f7c57a96d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts @@ -10,7 +10,7 @@ import { migratePackagePolicyToV7110 } from './to_v7_11.0'; describe('7.11.0 Endpoint Package Policy migration', () => { const migration = migratePackagePolicyToV7110; - it('adds malware notification checkbox and optional message', () => { + it('adds malware notification checkbox and optional message and adds AV registration config', () => { const doc: SavedObjectUnsanitizedDoc<PackagePolicy> = { attributes: { name: 'Some Policy Name', @@ -77,6 +77,7 @@ describe('7.11.0 Endpoint Package Policy migration', () => { policy: { value: { windows: { + antivirus_registration: { enabled: false }, popup: { malware: { message: '', diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts index 551e0ecfdcb4f..557633a747267 100644 --- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts +++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts @@ -23,8 +23,13 @@ export const migratePackagePolicyToV7110: SavedObjectMigrationFn<PackagePolicy, }, }; if (input && input.config) { - input.config.policy.value.windows.popup = popup; - input.config.policy.value.mac.popup = popup; + const policy = input.config.policy.value; + + policy.windows.antivirus_registration = policy.windows.antivirus_registration || { + enabled: false, + }; + policy.windows.popup = popup; + policy.mac.popup = popup; } } From 6900ce2b96027fa6f07aab6ad78ba654381a37b6 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <marta.bondyra@gmail.com> Date: Wed, 2 Dec 2020 19:55:58 +0100 Subject: [PATCH 067/107] [Lens] Provide single-value functions to show the "First" or "Last" value of some field (#83437) --- .../workspace_panel/warnings_popover.scss | 17 + .../workspace_panel/warnings_popover.tsx | 57 +++ .../workspace_panel_wrapper.tsx | 66 ++- .../dimension_panel/dimension_editor.scss | 4 + .../dimension_panel/dimension_editor.tsx | 28 +- .../dimension_panel/dimension_panel.test.tsx | 2 +- .../dimension_panel/dimension_panel.tsx | 18 +- .../dimension_panel/droppable.test.ts | 2 - .../indexpattern.test.ts | 12 +- .../indexpattern_datasource/indexpattern.tsx | 41 +- .../indexpattern_suggestions.ts | 6 +- .../indexpattern_datasource/loader.test.ts | 2 - .../operations/__mocks__/index.ts | 1 + .../operations/definitions/cardinality.tsx | 4 + .../operations/definitions/count.tsx | 3 + .../operations/definitions/date_histogram.tsx | 3 + .../operations/definitions/helpers.tsx | 34 ++ .../operations/definitions/index.ts | 40 +- .../definitions/last_value.test.tsx | 477 ++++++++++++++++++ .../operations/definitions/last_value.tsx | 257 ++++++++++ .../operations/definitions/metrics.tsx | 3 + .../operations/definitions/ranges/ranges.tsx | 3 + .../operations/definitions/terms/index.tsx | 5 +- .../definitions/terms/terms.test.tsx | 83 ++- .../operations/layer_helpers.test.ts | 1 - .../operations/mocks.ts | 1 + .../operations/operations.test.ts | 19 + .../public/indexpattern_datasource/utils.ts | 64 +-- .../pie_visualization/visualization.tsx | 28 + x-pack/plugins/lens/public/types.ts | 5 + .../xy_visualization/visualization.test.ts | 65 +++ .../public/xy_visualization/visualization.tsx | 32 ++ 32 files changed, 1256 insertions(+), 127 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.scss create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.scss new file mode 100644 index 0000000000000..19f815dfb7114 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.scss @@ -0,0 +1,17 @@ +.lnsWorkspaceWarning__button { + color: $euiColorWarningText; +} + +.lnsWorkspaceWarningList { + @include euiYScroll; + max-height: $euiSize * 20; + width: $euiSize * 16; +} + +.lnsWorkspaceWarningList__item { + padding: $euiSize; + + & + & { + border-top: $euiBorderThin; + } +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.tsx new file mode 100644 index 0000000000000..cb414972e84af --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/warnings_popover.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './workspace_panel_wrapper.scss'; +import './warnings_popover.scss'; + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopover, EuiText, EuiButtonEmpty } from '@elastic/eui'; + +export const WarningsPopover = ({ + children, +}: { + children?: React.ReactNode | React.ReactNode[]; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + if (!children) { + return null; + } + + const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const closePopover = () => setIsPopoverOpen(false); + const warningsCount = React.Children.count(children); + return ( + <EuiPopover + panelPaddingSize="none" + button={ + <EuiButtonEmpty + onClick={onButtonClick} + iconType="alert" + className="lnsWorkspaceWarning__button" + > + {i18n.translate('xpack.lens.chartWarnings.number', { + defaultMessage: `{warningsCount} {warningsCount, plural, one {warning} other {warnings}}`, + values: { + warningsCount, + }, + })} + </EuiButtonEmpty> + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > + <ul className="lnsWorkspaceWarningList"> + {React.Children.map(children, (child, index) => ( + <li key={index} className="lnsWorkspaceWarningList__item"> + <EuiText size="s">{child}</EuiText> + </li> + ))} + </ul> + </EuiPopover> + ); +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index d9fbaa22a0388..046bebb33a57d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -19,6 +19,7 @@ import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { Action } from '../state_management'; import { ChartSwitch } from './chart_switch'; +import { WarningsPopover } from './warnings_popover'; export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; @@ -64,40 +65,59 @@ export function WorkspacePanelWrapper({ }, [dispatch, activeVisualization] ); + const warningMessages = + activeVisualization?.getWarningMessages && + activeVisualization.getWarningMessages(visualizationState, framePublicAPI); return ( <> <div> <EuiFlexGroup + alignItems="center" gutterSize="m" direction="row" responsive={false} wrap={true} - className="lnsWorkspacePanelWrapper__toolbar" + justifyContent="spaceBetween" > <EuiFlexItem grow={false}> - <ChartSwitch - data-test-subj="lnsChartSwitcher" - visualizationMap={visualizationMap} - visualizationId={visualizationId} - visualizationState={visualizationState} - datasourceMap={datasourceMap} - datasourceStates={datasourceStates} - dispatch={dispatch} - framePublicAPI={framePublicAPI} - /> + <EuiFlexGroup + gutterSize="m" + direction="row" + responsive={false} + wrap={true} + className="lnsWorkspacePanelWrapper__toolbar" + > + <EuiFlexItem grow={false}> + <ChartSwitch + data-test-subj="lnsChartSwitcher" + visualizationMap={visualizationMap} + visualizationId={visualizationId} + visualizationState={visualizationState} + datasourceMap={datasourceMap} + datasourceStates={datasourceStates} + dispatch={dispatch} + framePublicAPI={framePublicAPI} + /> + </EuiFlexItem> + {activeVisualization && activeVisualization.renderToolbar && ( + <EuiFlexItem grow={false}> + <NativeRenderer + render={activeVisualization.renderToolbar} + nativeProps={{ + frame: framePublicAPI, + state: visualizationState, + setState: setVisualizationState, + }} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {warningMessages && warningMessages.length ? ( + <WarningsPopover>{warningMessages}</WarningsPopover> + ) : null} </EuiFlexItem> - {activeVisualization && activeVisualization.renderToolbar && ( - <EuiFlexItem grow={false}> - <NativeRenderer - render={activeVisualization.renderToolbar} - nativeProps={{ - frame: framePublicAPI, - state: visualizationState, - setState: setVisualizationState, - }} - /> - </EuiFlexItem> - )} </EuiFlexGroup> </div> <EuiPageContent className="lnsWorkspacePanelWrapper"> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss index 096047da681b9..6bd6808f17b35 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss @@ -15,6 +15,10 @@ column-gap: $euiSizeXL; } +.lnsIndexPatternDimensionEditor__operation .euiListGroupItem__label { + width: 100%; +} + .lnsIndexPatternDimensionEditor__operation > button { padding-top: 0; padding-bottom: 0; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 4c3def0e5bc7f..576825e9c960a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -15,6 +15,7 @@ import { EuiSpacer, EuiListGroupItemProps, EuiFormLabel, + EuiToolTip, } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; @@ -141,20 +142,19 @@ export function DimensionEditor(props: DimensionEditorProps) { definition.input === 'field' && fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || (selectedColumn && !hasField(selectedColumn) && definition.input === 'none'), + disabledStatus: + definition.getDisabledStatus && + definition.getDisabledStatus(state.indexPatterns[state.currentIndexPatternId]), }; }); - const selectedColumnSourceField = - selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined; - - const currentFieldIsInvalid = useMemo( - () => - fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern), - [selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern] - ); + const currentFieldIsInvalid = useMemo(() => fieldIsInvalid(selectedColumn, currentIndexPattern), [ + selectedColumn, + currentIndexPattern, + ]); const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map( - ({ operationType, compatibleWithCurrentField }) => { + ({ operationType, compatibleWithCurrentField, disabledStatus }) => { const isActive = Boolean( incompleteOperation === operationType || (!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType) @@ -168,7 +168,13 @@ export function DimensionEditor(props: DimensionEditorProps) { } let label: EuiListGroupItemProps['label'] = operationPanels[operationType].displayName; - if (isActive) { + if (disabledStatus) { + label = ( + <EuiToolTip content={disabledStatus} display="block" position="left"> + <span>{operationPanels[operationType].displayName}</span> + </EuiToolTip> + ); + } else if (isActive) { label = <strong>{operationPanels[operationType].displayName}</strong>; } @@ -178,6 +184,7 @@ export function DimensionEditor(props: DimensionEditorProps) { color, isActive, size: 's', + isDisabled: !!disabledStatus, className: 'lnsIndexPatternDimensionEditor__operation', 'data-test-subj': `lns-indexPatternDimension-${operationType}${ compatibleWithCurrentField ? '' : ' incompatible' @@ -264,7 +271,6 @@ export function DimensionEditor(props: DimensionEditorProps) { ? currentIndexPattern.getFieldByName(selectedColumn.sourceField) : undefined, }); - setState(mergeLayer({ state, layerId, newLayer })); }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index d4197f9395660..dbfffb5c2bd59 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1242,12 +1242,12 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([ 'Average', 'Count', + 'Last value', 'Maximum', 'Median', 'Minimum', 'Sum', 'Unique count', - '\u00a0', ]); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 2444a6a81c2a0..20134699d2067 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -12,7 +12,7 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternColumn } from '../indexpattern'; -import { fieldIsInvalid } from '../utils'; +import { isColumnInvalid } from '../utils'; import { IndexPatternPrivateState } from '../types'; import { DimensionEditor } from './dimension_editor'; import { DateRange } from '../../../common'; @@ -45,24 +45,22 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens ) { const layerId = props.layerId; const layer = props.state.layers[layerId]; - const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] || null; const currentIndexPattern = props.state.indexPatterns[layer.indexPatternId]; + const { columnId, uniqueLabel } = props; - const selectedColumnSourceField = - selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined; - const currentFieldIsInvalid = useMemo( - () => - fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern), - [selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern] + const currentColumnHasErrors = useMemo( + () => isColumnInvalid(layer, columnId, currentIndexPattern), + [layer, columnId, currentIndexPattern] ); - const { columnId, uniqueLabel } = props; + const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] || null; + if (!selectedColumn) { return null; } const formattedLabel = wrapOnDot(uniqueLabel); - if (currentFieldIsInvalid) { + if (currentColumnHasErrors) { return ( <EuiToolTip content={ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 48d8d55114115..6be03a92a445e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -17,8 +17,6 @@ import { OperationMetadata } from '../../types'; import { IndexPatternColumn } from '../operations'; import { getFieldByNameFactory } from '../pure_helpers'; -jest.mock('../operations'); - const fields = [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 3c1c8d5f2c006..20f71cfd3ce17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -819,7 +819,7 @@ describe('IndexPattern Data Source', () => { expect(messages).toHaveLength(1); expect(messages![0]).toEqual({ shortMessage: 'Invalid reference.', - longMessage: 'Field "bytes" has an invalid reference.', + longMessage: '"Foo" has an invalid reference.', }); }); @@ -844,7 +844,7 @@ describe('IndexPattern Data Source', () => { col2: { dataType: 'number', isBucketed: false, - label: 'Foo', + label: 'Foo2', operationType: 'count', // <= invalid sourceField: 'memory', }, @@ -857,7 +857,7 @@ describe('IndexPattern Data Source', () => { expect(messages).toHaveLength(1); expect(messages![0]).toEqual({ shortMessage: 'Invalid references.', - longMessage: 'Fields "bytes", "memory" have invalid reference.', + longMessage: '"Foo", "Foo2" have invalid reference.', }); }); @@ -882,7 +882,7 @@ describe('IndexPattern Data Source', () => { col2: { dataType: 'number', isBucketed: false, - label: 'Foo', + label: 'Foo2', operationType: 'count', // <= invalid sourceField: 'memory', }, @@ -909,11 +909,11 @@ describe('IndexPattern Data Source', () => { expect(messages).toEqual([ { shortMessage: 'Invalid references on Layer 1.', - longMessage: 'Layer 1 has invalid references in fields "bytes", "memory".', + longMessage: 'Layer 1 has invalid references in "Foo", "Foo2".', }, { shortMessage: 'Invalid reference on Layer 2.', - longMessage: 'Layer 2 has an invalid reference in field "source".', + longMessage: 'Layer 2 has an invalid reference in "Foo".', }, ]); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 948619c6b07e5..a639ea2c00ac0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -40,7 +40,7 @@ import { } from './indexpattern_suggestions'; import { - getInvalidFieldsForLayer, + getInvalidColumnsForLayer, getInvalidLayers, isDraggedField, normalizeOperationDataType, @@ -387,7 +387,7 @@ export function getIndexPatternDatasource({ } }) .filter(Boolean) as Array<[number, number]>; - const invalidFieldsPerLayer: string[][] = getInvalidFieldsForLayer( + const invalidColumnsForLayer: string[][] = getInvalidColumnsForLayer( invalidLayers, state.indexPatterns ); @@ -397,33 +397,34 @@ export function getIndexPatternDatasource({ return [ ...layerErrors, ...realIndex.map(([filteredIndex, layerIndex]) => { - const fieldsWithBrokenReferences: string[] = invalidFieldsPerLayer[filteredIndex].map( - (columnId) => { - const column = invalidLayers[filteredIndex].columns[ - columnId - ] as FieldBasedIndexPatternColumn; - return column.sourceField; - } - ); + const columnLabelsWithBrokenReferences: string[] = invalidColumnsForLayer[ + filteredIndex + ].map((columnId) => { + const column = invalidLayers[filteredIndex].columns[ + columnId + ] as FieldBasedIndexPatternColumn; + return column.label; + }); if (originalLayersList.length === 1) { return { shortMessage: i18n.translate( 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', { - defaultMessage: 'Invalid {fields, plural, one {reference} other {references}}.', + defaultMessage: + 'Invalid {columns, plural, one {reference} other {references}}.', values: { - fields: fieldsWithBrokenReferences.length, + columns: columnLabelsWithBrokenReferences.length, }, } ), longMessage: i18n.translate( 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', { - defaultMessage: `{fieldsLength, plural, one {Field} other {Fields}} "{fields}" {fieldsLength, plural, one {has an} other {have}} invalid reference.`, + defaultMessage: `"{columns}" {columnsLength, plural, one {has an} other {have}} invalid reference.`, values: { - fields: fieldsWithBrokenReferences.join('", "'), - fieldsLength: fieldsWithBrokenReferences.length, + columns: columnLabelsWithBrokenReferences.join('", "'), + columnsLength: columnLabelsWithBrokenReferences.length, }, } ), @@ -432,18 +433,18 @@ export function getIndexPatternDatasource({ return { shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { defaultMessage: - 'Invalid {fieldsLength, plural, one {reference} other {references}} on Layer {layer}.', + 'Invalid {columnsLength, plural, one {reference} other {references}} on Layer {layer}.', values: { layer: layerIndex, - fieldsLength: fieldsWithBrokenReferences.length, + columnsLength: columnLabelsWithBrokenReferences.length, }, }), longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { - defaultMessage: `Layer {layer} has {fieldsLength, plural, one {an invalid} other {invalid}} {fieldsLength, plural, one {reference} other {references}} in {fieldsLength, plural, one {field} other {fields}} "{fields}".`, + defaultMessage: `Layer {layer} has {columnsLength, plural, one {an invalid} other {invalid}} {columnsLength, plural, one {reference} other {references}} in "{columns}".`, values: { layer: layerIndex, - fields: fieldsWithBrokenReferences.join('", "'), - fieldsLength: fieldsWithBrokenReferences.length, + columns: columnLabelsWithBrokenReferences.join('", "'), + columnsLength: columnLabelsWithBrokenReferences.length, }, }), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 263b4646c9feb..ebac396210a5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -18,7 +18,7 @@ import { IndexPatternColumn, OperationType, } from './operations'; -import { hasField, hasInvalidFields } from './utils'; +import { hasField, hasInvalidColumns } from './utils'; import { IndexPattern, IndexPatternPrivateState, @@ -90,7 +90,7 @@ export function getDatasourceSuggestionsForField( indexPatternId: string, field: IndexPatternField ): IndexPatternSugestion[] { - if (hasInvalidFields(state)) return []; + if (hasInvalidColumns(state)) return []; const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); @@ -331,7 +331,7 @@ function createNewLayerWithMetricAggregation( export function getDatasourceSuggestionsFromCurrentState( state: IndexPatternPrivateState ): Array<DatasourceSuggestion<IndexPatternPrivateState>> { - if (hasInvalidFields(state)) return []; + if (hasInvalidColumns(state)) return []; const layers = Object.entries(state.layers || {}); if (layers.length > 1) { // Return suggestions that reduce the data to each layer individually diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index adb86253ab28c..29786d9bc68f3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -25,8 +25,6 @@ import { import { createMockedRestrictedIndexPattern, createMockedIndexPattern } from './mocks'; import { documentField } from './document_field'; -jest.mock('./operations'); - const createMockStorage = (lastData?: Record<string, string>) => { return { get: jest.fn().mockImplementation(() => lastData), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 077255b75a769..ff900134df9a1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -24,6 +24,7 @@ export const { getOperationResultType, operationDefinitionMap, operationDefinitions, + getInvalidFieldMessage, } = actualOperations; export const { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index fd3ca4319669e..2dc3946c62a09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; +import { getInvalidFieldMessage } from './helpers'; + const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); const SCALE = 'ratio'; @@ -42,6 +44,8 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo return { dataType: 'number', isBucketed: IS_BUCKETED, scale: SCALE }; } }, + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.getFieldByName(column.sourceField); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 8cb95de72f97e..02a69ad8e550f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; +import { getInvalidFieldMessage } from './helpers'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, @@ -29,6 +30,8 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field defaultMessage: 'Count', }), input: 'field', + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index efac9c151a435..ca426fb53a3ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -23,6 +23,7 @@ import { updateColumnParam } from '../layer_helpers'; import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternAggRestrictions, search } from '../../../../../../../src/plugins/data/public'; +import { getInvalidFieldMessage } from './helpers'; const { isValidInterval } = search.aggs; const autoInterval = 'auto'; @@ -46,6 +47,8 @@ export const dateHistogramOperation: OperationDefinition< }), input: 'field', priority: 5, // Highest priority level used + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'date' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index a5c08a93467af..640a357d9a7a4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -6,6 +6,10 @@ import { useRef } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; +import { i18n } from '@kbn/i18n'; +import { operationDefinitionMap } from '.'; +import { FieldBasedIndexPatternColumn } from './column_types'; +import { IndexPattern } from '../../types'; export const useDebounceWithOptions = ( fn: Function, @@ -28,3 +32,33 @@ export const useDebounceWithOptions = ( newDeps ); }; + +export function getInvalidFieldMessage( + column: FieldBasedIndexPatternColumn, + indexPattern?: IndexPattern +) { + if (!indexPattern) { + return; + } + const { sourceField, operationType } = column; + const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined; + const operationDefinition = operationType && operationDefinitionMap[operationType]; + + const isInvalid = Boolean( + sourceField && + operationDefinition && + !( + field && + operationDefinition?.input === 'field' && + operationDefinition.getPossibleOperationForField(field) !== undefined + ) + ); + return isInvalid + ? [ + i18n.translate('xpack.lens.indexPattern.fieldNotFound', { + defaultMessage: 'Field {invalidField} was not found', + values: { invalidField: sourceField }, + }), + ] + : undefined; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 31bb332f791da..460c7c5492879 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -34,6 +34,7 @@ import { MovingAverageIndexPatternColumn, } from './calculations'; import { countOperation, CountIndexPatternColumn } from './count'; +import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; import { StateSetter, OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { @@ -63,6 +64,7 @@ export type IndexPatternColumn = | SumIndexPatternColumn | MedianIndexPatternColumn | CountIndexPatternColumn + | LastValueIndexPatternColumn | CumulativeSumIndexPatternColumn | CounterRateIndexPatternColumn | DerivativeIndexPatternColumn @@ -85,6 +87,7 @@ const internalOperationDefinitions = [ cardinalityOperation, sumOperation, medianOperation, + lastValueOperation, countOperation, rangeOperation, cumulativeSumOperation, @@ -99,6 +102,7 @@ export { filtersOperation } from './filters'; export { dateHistogramOperation } from './date_histogram'; export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; export { countOperation } from './count'; +export { lastValueOperation } from './last_value'; export { cumulativeSumOperation, counterRateOperation, @@ -173,6 +177,24 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> { */ transfer?: (column: C, newIndexPattern: IndexPattern) => C; /** + * if there is some reason to display the operation in the operations list + * but disable it from usage, this function returns the string describing + * the status. Otherwise it returns undefined + */ + getDisabledStatus?: (indexPattern: IndexPattern) => string | undefined; + /** + * Validate that the operation has the right preconditions in the state. For example: + * + * - Requires a date histogram operation somewhere before it in order + * - Missing references + */ + getErrorMessage?: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern?: IndexPattern + ) => string[] | undefined; + + /* * Flag whether this operation can be scaled by time unit if a date histogram is available. * If set to mandatory or optional, a UI element is shown in the config flyout to configure the time unit * to scale by. The chosen unit will be persisted as `timeScale` property of the column. @@ -245,6 +267,17 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> { * together with the agg configs returned from other columns. */ toEsAggsConfig: (column: C, columnId: string, indexPattern: IndexPattern) => unknown; + /** + * Validate that the operation has the right preconditions in the state. For example: + * + * - Requires a date histogram operation somewhere before it in order + * - Missing references + */ + getErrorMessage: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern?: IndexPattern + ) => string[] | undefined; } export interface RequiredReference { @@ -297,13 +330,6 @@ interface FullReferenceOperationDefinition<C extends BaseIndexPatternColumn> { columnId: string, indexPattern: IndexPattern ) => ExpressionFunctionAST[]; - /** - * Validate that the operation has the right preconditions in the state. For example: - * - * - Requires a date histogram operation somewhere before it in order - * - Missing references - */ - getErrorMessage?: (layer: IndexPatternLayer, columnId: string) => string[] | undefined; } interface OperationDefinitionMap<C extends BaseIndexPatternColumn> { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx new file mode 100644 index 0000000000000..09b68e78d3469 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -0,0 +1,477 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiComboBox } from '@elastic/eui'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createMockedIndexPattern } from '../../mocks'; +import { LastValueIndexPatternColumn } from './last_value'; +import { lastValueOperation } from './index'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from '../../types'; + +const defaultProps = { + storage: {} as IStorageWrapper, + uiSettings: {} as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + data: dataPluginMock.createStartContract(), + http: {} as HttpSetup, +}; + +describe('last_value', () => { + let state: IndexPatternPrivateState; + const InlineOptions = lastValueOperation.paramEditor!; + + beforeEach(() => { + const indexPattern = createMockedIndexPattern(); + state = { + indexPatternRefs: [], + indexPatterns: { + '1': { + ...indexPattern, + hasRestrictions: false, + } as IndexPattern, + }, + existingFields: {}, + currentIndexPatternId: '1', + isFirstExistenceFetch: false, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + col2: { + label: 'Last value of a', + dataType: 'number', + isBucketed: false, + sourceField: 'a', + operationType: 'last_value', + params: { + sortField: 'datefield', + }, + }, + }, + }, + }, + }; + }); + + describe('toEsAggsConfig', () => { + it('should reflect params correctly', () => { + const lastValueColumn = state.layers.first.columns.col2 as LastValueIndexPatternColumn; + const esAggsConfig = lastValueOperation.toEsAggsConfig( + { ...lastValueColumn, params: { ...lastValueColumn.params } }, + 'col1', + {} as IndexPattern + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ + aggregate: 'concat', + field: 'a', + size: 1, + sortField: 'datefield', + sortOrder: 'desc', + }), + }) + ); + }); + }); + + describe('onFieldChange', () => { + it('should change correctly to new field', () => { + const oldColumn: LastValueIndexPatternColumn = { + operationType: 'last_value', + sourceField: 'source', + label: 'Last value of source', + isBucketed: true, + dataType: 'string', + params: { + sortField: 'datefield', + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('bytes')!; + const column = lastValueOperation.onFieldChange(oldColumn, newNumberField); + + expect(column).toEqual( + expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + params: expect.objectContaining({ + sortField: 'datefield', + }), + }) + ); + expect(column.label).toContain('bytes'); + }); + + it('should remove numeric parameters when changing away from number', () => { + const oldColumn: LastValueIndexPatternColumn = { + operationType: 'last_value', + sourceField: 'bytes', + label: 'Last value of bytes', + isBucketed: false, + dataType: 'number', + params: { + sortField: 'datefield', + }, + }; + const indexPattern = createMockedIndexPattern(); + const newStringField = indexPattern.fields.find((i) => i.name === 'source')!; + + const column = lastValueOperation.onFieldChange(oldColumn, newStringField); + expect(column).toHaveProperty('dataType', 'string'); + expect(column).toHaveProperty('sourceField', 'source'); + expect(column.params.format).toBeUndefined(); + }); + }); + + describe('getPossibleOperationForField', () => { + it('should return operation with the right type', () => { + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'boolean', + }) + ).toEqual({ + dataType: 'boolean', + isBucketed: false, + scale: 'ratio', + }); + + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'ip', + }) + ).toEqual({ + dataType: 'ip', + isBucketed: false, + scale: 'ratio', + }); + }); + + it('should not return an operation if restrictions prevent terms', () => { + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }) + ).toEqual(undefined); + + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: true, + aggregationRestrictions: {}, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + }) + ).toEqual(undefined); + // does it have to be aggregatable? + expect( + lastValueOperation.getPossibleOperationForField({ + aggregatable: false, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + }) + ).toEqual({ dataType: 'string', isBucketed: false, scale: 'ordinal' }); + }); + }); + + describe('buildColumn', () => { + it('should use type from the passed field', () => { + const lastValueColumn = lastValueOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + field: { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }); + expect(lastValueColumn.dataType).toEqual('boolean'); + }); + + it('should use indexPattern timeFieldName as a default sortField', () => { + const lastValueColumn = lastValueOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + + layer: { + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + columnOrder: [], + indexPatternId: '', + }, + + field: { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + }); + expect(lastValueColumn.params).toEqual( + expect.objectContaining({ + sortField: 'timestamp', + }) + ); + }); + + it('should use first indexPattern date field if there is no default timefieldName', () => { + const indexPattern = createMockedIndexPattern(); + const indexPatternNoTimeField = { + ...indexPattern, + timeFieldName: undefined, + fields: [ + { + aggregatable: true, + searchable: true, + type: 'date', + name: 'datefield', + displayName: 'datefield', + }, + { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + ], + }; + const lastValueColumn = lastValueOperation.buildColumn({ + indexPattern: indexPatternNoTimeField, + + layer: { + columns: { + col1: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + columnOrder: [], + indexPatternId: '', + }, + + field: { + aggregatable: true, + searchable: true, + type: 'boolean', + name: 'test', + displayName: 'test', + }, + }); + expect(lastValueColumn.params).toEqual( + expect.objectContaining({ + sortField: 'datefield', + }) + ); + }); + }); + + it('should return disabledStatus if indexPattern does contain date field', () => { + const indexPattern = createMockedIndexPattern(); + + expect(lastValueOperation.getDisabledStatus!(indexPattern)).toEqual(undefined); + + const indexPatternWithoutTimeFieldName = { + ...indexPattern, + timeFieldName: undefined, + }; + expect(lastValueOperation.getDisabledStatus!(indexPatternWithoutTimeFieldName)).toEqual( + undefined + ); + + const indexPatternWithoutTimefields = { + ...indexPatternWithoutTimeFieldName, + fields: indexPattern.fields.filter((f) => f.type !== 'date'), + }; + + const disabledStatus = lastValueOperation.getDisabledStatus!(indexPatternWithoutTimefields); + expect(disabledStatus).toEqual( + 'This function requires the presence of a date field in your index' + ); + }); + + describe('param editor', () => { + it('should render current sortField', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + <InlineOptions + {...defaultProps} + state={state} + setState={setStateSpy} + columnId="col1" + currentColumn={state.layers.first.columns.col2 as LastValueIndexPatternColumn} + layerId="first" + /> + ); + + const select = instance.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]'); + + expect(select.prop('selectedOptions')).toEqual([{ label: 'datefield', value: 'datefield' }]); + }); + + it('should update state when changing sortField', () => { + const setStateSpy = jest.fn(); + const instance = shallow( + <InlineOptions + {...defaultProps} + state={state} + setState={setStateSpy} + columnId="col1" + currentColumn={state.layers.first.columns.col2 as LastValueIndexPatternColumn} + layerId="first" + /> + ); + + instance + .find('[data-test-subj="lns-indexPattern-lastValue-sortField"]') + .find(EuiComboBox) + .prop('onChange')!([{ label: 'datefield2', value: 'datefield2' }]); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + ...state.layers.first.columns.col2, + params: { + ...(state.layers.first.columns.col2 as LastValueIndexPatternColumn).params, + sortField: 'datefield2', + }, + }, + }, + }, + }, + }); + }); + }); + + describe('getErrorMessage', () => { + let indexPattern: IndexPattern; + let layer: IndexPatternLayer; + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + layer = { + columns: { + col1: { + dataType: 'boolean', + isBucketed: false, + label: 'Last value of test', + operationType: 'last_value', + params: { sortField: 'timestamp' }, + scale: 'ratio', + sourceField: 'bytes', + }, + }, + columnOrder: [], + indexPatternId: '', + }; + }); + it('returns undefined if sourceField exists and sortField is of type date ', () => { + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual(undefined); + }); + it('shows error message if the sourceField does not exist in index pattern', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + sourceField: 'notExisting', + } as LastValueIndexPatternColumn, + }, + }; + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field notExisting was not found', + ]); + }); + it('shows error message if the sortField does not exist in index pattern', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + params: { + ...layer.columns.col1.params, + sortField: 'notExisting', + }, + } as LastValueIndexPatternColumn, + }, + }; + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field notExisting was not found', + ]); + }); + it('shows error message if the sortField is not date', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + params: { + ...layer.columns.col1.params, + sortField: 'bytes', + }, + } as LastValueIndexPatternColumn, + }, + }; + expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field bytes is not a date field and cannot be used for sorting', + ]); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx new file mode 100644 index 0000000000000..5ae5dd472ce22 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { OperationDefinition } from './index'; +import { FieldBasedIndexPatternColumn } from './column_types'; +import { IndexPatternField, IndexPattern } from '../../types'; +import { updateColumnParam } from '../layer_helpers'; +import { DataType } from '../../../types'; +import { getInvalidFieldMessage } from './helpers'; + +function ofName(name: string) { + return i18n.translate('xpack.lens.indexPattern.lastValueOf', { + defaultMessage: 'Last value of {name}', + values: { name }, + }); +} + +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); + +export function getInvalidSortFieldMessage(sortField: string, indexPattern?: IndexPattern) { + if (!indexPattern) { + return; + } + const field = indexPattern.getFieldByName(sortField); + if (!field) { + return i18n.translate('xpack.lens.indexPattern.lastValue.sortFieldNotFound', { + defaultMessage: 'Field {invalidField} was not found', + values: { invalidField: sortField }, + }); + } + if (field.type !== 'date') { + return i18n.translate('xpack.lens.indexPattern.lastValue.invalidTypeSortField', { + defaultMessage: 'Field {invalidField} is not a date field and cannot be used for sorting', + values: { invalidField: sortField }, + }); + } +} + +function isTimeFieldNameDateField(indexPattern: IndexPattern) { + return ( + indexPattern.timeFieldName && + indexPattern.fields.find( + (field) => field.name === indexPattern.timeFieldName && field.type === 'date' + ) + ); +} + +function getDateFields(indexPattern: IndexPattern): IndexPatternField[] { + const dateFields = indexPattern.fields.filter((field) => field.type === 'date'); + if (isTimeFieldNameDateField(indexPattern)) { + dateFields.sort(({ name: nameA }, { name: nameB }) => { + if (nameA === indexPattern.timeFieldName) { + return -1; + } + if (nameB === indexPattern.timeFieldName) { + return 1; + } + return 0; + }); + } + return dateFields; +} + +export interface LastValueIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'last_value'; + params: { + sortField: string; + // last value on numeric fields can be formatted + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn, 'field'> = { + type: 'last_value', + displayName: i18n.translate('xpack.lens.indexPattern.lastValue', { + defaultMessage: 'Last value', + }), + getDefaultLabel: (column, indexPattern) => + indexPattern.getFieldByName(column.sourceField)!.displayName, + input: 'field', + onFieldChange: (oldColumn, field) => { + const newParams = { ...oldColumn.params }; + + if ('format' in newParams && field.type !== 'number') { + delete newParams.format; + } + return { + ...oldColumn, + dataType: field.type as DataType, + label: ofName(field.displayName), + sourceField: field.name, + params: newParams, + }; + }, + getPossibleOperationForField: ({ aggregationRestrictions, type }) => { + if (supportedTypes.has(type) && !aggregationRestrictions) { + return { + dataType: type as DataType, + isBucketed: false, + scale: type === 'string' ? 'ordinal' : 'ratio', + }; + } + }, + getDisabledStatus(indexPattern: IndexPattern) { + const hasDateFields = indexPattern && getDateFields(indexPattern).length; + if (!hasDateFields) { + return i18n.translate('xpack.lens.indexPattern.lastValue.disabled', { + defaultMessage: 'This function requires the presence of a date field in your index', + }); + } + }, + getErrorMessage(layer, columnId, indexPattern) { + const column = layer.columns[columnId] as LastValueIndexPatternColumn; + let errorMessages: string[] = []; + const invalidSourceFieldMessage = getInvalidFieldMessage(column, indexPattern); + const invalidSortFieldMessage = getInvalidSortFieldMessage( + column.params.sortField, + indexPattern + ); + if (invalidSourceFieldMessage) { + errorMessages = [...invalidSourceFieldMessage]; + } + if (invalidSortFieldMessage) { + errorMessages = [invalidSortFieldMessage]; + } + return errorMessages.length ? errorMessages : undefined; + }, + buildColumn({ field, previousColumn, indexPattern }) { + const sortField = isTimeFieldNameDateField(indexPattern) + ? indexPattern.timeFieldName + : indexPattern.fields.find((f) => f.type === 'date')?.name; + + if (!sortField) { + throw new Error( + i18n.translate('xpack.lens.functions.lastValue.missingSortField', { + defaultMessage: 'This index pattern does not contain any date fields', + }) + ); + } + + return { + label: ofName(field.displayName), + dataType: field.type as DataType, + operationType: 'last_value', + isBucketed: false, + scale: field.type === 'string' ? 'ordinal' : 'ratio', + sourceField: field.name, + params: { + sortField, + }, + }; + }, + toEsAggsConfig: (column, columnId) => ({ + id: columnId, + enabled: true, + schema: 'metric', + type: 'top_hits', + params: { + field: column.sourceField, + aggregate: 'concat', + size: 1, + sortOrder: 'desc', + sortField: column.params.sortField, + }, + }), + + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.getFieldByName(column.sourceField); + const newTimeField = newIndexPattern.getFieldByName(column.params.sortField); + return Boolean( + newField && + newField.type === column.dataType && + !newField.aggregationRestrictions && + newTimeField?.type === 'date' + ); + }, + + paramEditor: ({ state, setState, currentColumn, layerId }) => { + const currentIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + const dateFields = getDateFields(currentIndexPattern); + const isSortFieldInvalid = !!getInvalidSortFieldMessage( + currentColumn.params.sortField, + currentIndexPattern + ); + return ( + <> + <EuiFormRow + label={i18n.translate('xpack.lens.indexPattern.lastValue.sortField', { + defaultMessage: 'Sort by date field', + })} + display="columnCompressed" + fullWidth + error={i18n.translate('xpack.lens.indexPattern.sortField.invalid', { + defaultMessage: 'Invalid field. Check your index pattern or pick another field.', + })} + isInvalid={isSortFieldInvalid} + > + <EuiComboBox + placeholder={i18n.translate('xpack.lens.indexPattern.lastValue.sortFieldPlaceholder', { + defaultMessage: 'Sort field', + })} + compressed + isClearable={false} + data-test-subj="lns-indexPattern-lastValue-sortField" + isInvalid={isSortFieldInvalid} + singleSelection={{ asPlainText: true }} + aria-label={i18n.translate('xpack.lens.indexPattern.lastValue.sortField', { + defaultMessage: 'Sort by date field', + })} + options={dateFields?.map((field: IndexPatternField) => { + return { + value: field.name, + label: field.displayName, + }; + })} + onChange={(choices) => { + if (choices.length === 0) { + return; + } + setState( + updateColumnParam({ + state, + layerId, + currentColumn, + paramName: 'sortField', + value: choices[0].value, + }) + ); + }} + selectedOptions={ + ((currentColumn.params?.sortField + ? [ + { + label: + currentIndexPattern.getFieldByName(currentColumn.params.sortField) + ?.displayName || currentColumn.params.sortField, + value: currentColumn.params.sortField, + }, + ] + : []) as unknown) as EuiComboBoxOptionOption[] + } + /> + </EuiFormRow> + </> + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 45ba721981ed5..10a0b915b552d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { OperationDefinition } from './index'; +import { getInvalidFieldMessage } from './helpers'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn, @@ -103,6 +104,8 @@ function buildMetricOperation<T extends MetricColumn<string>>({ missing: 0, }, }), + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), } as OperationDefinition<T, 'field'>; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index d2456e1c8d375..f2d3435cc52c0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -17,6 +17,7 @@ import { mergeLayer } from '../../../state_helpers'; import { supportedFormats } from '../../../format_column'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; import { IndexPattern, IndexPatternField } from '../../../types'; +import { getInvalidFieldMessage } from '../helpers'; type RangeType = Omit<Range, 'type'>; // Try to cover all possible serialized states for ranges @@ -109,6 +110,8 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field }), priority: 4, // Higher than terms, so numbers get histogram input: 'field', + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'number' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 7c69a70c09351..e8351ea1e1d09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -22,6 +22,7 @@ import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesRangeInput } from './values_range_input'; +import { getInvalidFieldMessage } from '../helpers'; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.termsOf', { @@ -31,7 +32,7 @@ function ofName(name: string) { } function isSortableByColumn(column: IndexPatternColumn) { - return !column.isBucketed; + return !column.isBucketed && column.operationType !== 'last_value'; } const DEFAULT_SIZE = 3; @@ -71,6 +72,8 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field return { dataType: type as DataType, isBucketed: true, scale: 'ordinal' }; } }, + getErrorMessage: (layer, columnId, indexPattern) => + getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.getFieldByName(column.sourceField); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index e43c7bbd2f72e..0af0f9a9d8613 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -15,7 +15,7 @@ import { createMockedIndexPattern } from '../../../mocks'; import { ValuesRangeInput } from './values_range_input'; import { TermsIndexPatternColumn } from '.'; import { termsOperation } from '../index'; -import { IndexPatternPrivateState, IndexPattern } from '../../../types'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from '../../../types'; const defaultProps = { storage: {} as IStorageWrapper, @@ -368,7 +368,7 @@ describe('terms', () => { }); describe('onOtherColumnChanged', () => { - it('should keep the column if order by column still exists and is metric', () => { + it('should keep the column if order by column still exists and is isSortableByColumn metric', () => { const initialColumn: TermsIndexPatternColumn = { label: 'Top value of category', dataType: 'string', @@ -395,6 +395,40 @@ describe('terms', () => { expect(updatedColumn).toBe(initialColumn); }); + it('should switch to alphabetical ordering if metric is of type last_value', () => { + const initialColumn: TermsIndexPatternColumn = { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col1' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }; + const updatedColumn = termsOperation.onOtherColumnChanged!(initialColumn, { + col1: { + label: 'Last Value', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'last_value', + params: { + sortField: 'time', + }, + }, + }); + expect(updatedColumn.params).toEqual( + expect.objectContaining({ + orderBy: { type: 'alphabetical' }, + }) + ); + }); + it('should switch to alphabetical ordering if there are no columns to order by', () => { const termsColumn = termsOperation.onOtherColumnChanged!( { @@ -770,4 +804,49 @@ describe('terms', () => { }); }); }); + describe('getErrorMessage', () => { + let indexPattern: IndexPattern; + let layer: IndexPatternLayer; + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + layer = { + columns: { + col1: { + dataType: 'boolean', + isBucketed: true, + label: 'Top values of bytes', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + otherBucket: true, + size: 5, + }, + scale: 'ordinal', + sourceField: 'bytes', + }, + }, + columnOrder: [], + indexPatternId: '', + }; + }); + it('returns undefined if sourceField exists in index pattern', () => { + expect(termsOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual(undefined); + }); + it('returns error message if the sourceField does not exist in index pattern', () => { + layer = { + ...layer, + columns: { + col1: { + ...layer.columns.col1, + sourceField: 'notExisting', + } as TermsIndexPatternColumn, + }, + }; + expect(termsOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([ + 'Field notExisting was not found', + ]); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 7ffbeac39c6f5..93447053a6029 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -1706,7 +1706,6 @@ describe('state_helpers', () => { describe('getErrorMessages', () => { it('should collect errors from the operation definitions', () => { const mock = jest.fn().mockReturnValue(['error 1']); - // @ts-expect-error not statically analyzed operationDefinitionMap.testReference.getErrorMessage = mock; const errors = getErrorMessages({ indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts index c3f7dac03ada3..33af8842648f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -35,5 +35,6 @@ export const createMockedReferenceOperation = () => { toExpression: jest.fn().mockReturnValue([]), getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), getDefaultLabel: jest.fn().mockReturnValue('Default label'), + getErrorMessage: jest.fn(), }; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 63d0fd3d4e5c5..9f2b8eab4e09b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -293,6 +293,25 @@ describe('getOperationTypesForField', () => { "operationType": "median", "type": "field", }, + Object { + "field": "bytes", + "operationType": "last_value", + "type": "field", + }, + ], + }, + Object { + "operationMetaData": Object { + "dataType": "string", + "isBucketed": false, + "scale": "ordinal", + }, + "operations": Array [ + Object { + "field": "source", + "operationType": "last_value", + "type": "field", + }, ], }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 01b834610eb1a..5f4865ca0ac32 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -11,7 +11,9 @@ import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, } from './operations/definitions/column_types'; -import { operationDefinitionMap, OperationType } from './operations'; +import { operationDefinitionMap, IndexPatternColumn } from './operations'; + +import { getInvalidFieldMessage } from './operations/definitions/helpers'; /** * Normalizes the specified operation type. (e.g. document operations @@ -42,60 +44,46 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg ); } -export function hasInvalidFields(state: IndexPatternPrivateState) { +export function hasInvalidColumns(state: IndexPatternPrivateState) { return getInvalidLayers(state).length > 0; } export function getInvalidLayers(state: IndexPatternPrivateState) { return Object.values(state.layers).filter((layer) => { - return layer.columnOrder.some((columnId) => { - const column = layer.columns[columnId]; - return ( - hasField(column) && - fieldIsInvalid( - column.sourceField, - column.operationType, - state.indexPatterns[layer.indexPatternId] - ) - ); - }); + return layer.columnOrder.some((columnId) => + isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]) + ); }); } -export function getInvalidFieldsForLayer( +export function getInvalidColumnsForLayer( layers: IndexPatternLayer[], indexPatternMap: Record<string, IndexPattern> ) { return layers.map((layer) => { - return layer.columnOrder.filter((columnId) => { - const column = layer.columns[columnId]; - return ( - hasField(column) && - fieldIsInvalid( - column.sourceField, - column.operationType, - indexPatternMap[layer.indexPatternId] - ) - ); - }); + return layer.columnOrder.filter((columnId) => + isColumnInvalid(layer, columnId, indexPatternMap[layer.indexPatternId]) + ); }); } -export function fieldIsInvalid( - sourceField: string | undefined, - operationType: OperationType | undefined, +export function isColumnInvalid( + layer: IndexPatternLayer, + columnId: string, indexPattern: IndexPattern ) { - const operationDefinition = operationType && operationDefinitionMap[operationType]; - const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined; + const column = layer.columns[columnId]; - return Boolean( - sourceField && - operationDefinition && - !( - field && - operationDefinition?.input === 'field' && - operationDefinition.getPossibleOperationForField(field) !== undefined - ) + const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; + return !!( + operationDefinition.getErrorMessage && + operationDefinition.getErrorMessage(layer, columnId, indexPattern) ); } + +export function fieldIsInvalid(column: IndexPatternColumn | undefined, indexPattern: IndexPattern) { + if (!column || !hasField(column)) { + return false; + } + return !!getInvalidFieldMessage(column, indexPattern)?.length; +} diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 91f0ddb54ad41..2d9a345b978ec 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -245,6 +245,34 @@ export const getPieVisualization = ({ ); }, + getWarningMessages(state, frame) { + if (state?.layers.length === 0 || !frame.activeData) { + return; + } + + const metricColumnsWithArrayValues = []; + + for (const layer of state.layers) { + const { layerId, metric } = layer; + const rows = frame.activeData[layerId] && frame.activeData[layerId].rows; + if (!rows || !metric) { + break; + } + const columnToLabel = frame.datasourceLayers[layerId].getOperationForColumnId(metric)?.label; + + const hasArrayValues = rows.some((row) => Array.isArray(row[metric])); + if (hasArrayValues) { + metricColumnsWithArrayValues.push(columnToLabel || metric); + } + } + return metricColumnsWithArrayValues.map((label) => ( + <> + <strong>{label}</strong> contains array values. Your visualization may not render as + expected. + </> + )); + }, + getErrorMessages(state, frame) { // not possible to break it? return undefined; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 2b3cd02c09d1e..ba459a73ea0ee 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -598,6 +598,11 @@ export interface Visualization<T = unknown> { state: T, frame: FramePublicAPI ) => Array<{ shortMessage: string; longMessage: string }> | undefined; + + /** + * The frame calls this function to display warnings about visualization + */ + getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined; } export interface LensFilterEvent { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 546cf06d4014e..d780ce85bad69 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -775,4 +775,69 @@ describe('xy_visualization', () => { ]); }); }); + + describe('#getWarningMessages', () => { + let mockDatasource: ReturnType<typeof createMockDatasource>; + let frame: ReturnType<typeof createMockFramePublicAPI>; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + frame.activeData = { + first: { + type: 'datatable', + columns: [ + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, + ], + rows: [ + { a: 1, b: [2, 0] }, + { a: 3, b: 4 }, + { a: 5, b: 6 }, + { a: 7, b: 8 }, + ], + }, + }; + }); + it('should return a warning when numeric accessors contain array', () => { + (frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({ + label: 'Label B', + }); + const warningMessages = xyVisualization.getWarningMessages!( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: ['b'], + }, + ], + }, + frame + ); + expect(warningMessages).toHaveLength(1); + expect(warningMessages && warningMessages[0]).toMatchInlineSnapshot(` + <React.Fragment> + <strong> + Label B + </strong> + contains array values. Your visualization may not render as expected. + </React.Fragment> + `); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index f0dcaf589b1c4..ebf80c61e0cd1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -22,6 +22,7 @@ import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; +import { getColumnToLabelMap } from './state_helpers'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -371,6 +372,37 @@ export const getXyVisualization = ({ return errors.length ? errors : undefined; }, + + getWarningMessages(state, frame) { + if (state?.layers.length === 0 || !frame.activeData) { + return; + } + + const layers = state.layers; + + const filteredLayers = layers.filter(({ accessors }: LayerConfig) => accessors.length > 0); + const accessorsWithArrayValues = []; + for (const layer of filteredLayers) { + const { layerId, accessors } = layer; + const rows = frame.activeData[layerId] && frame.activeData[layerId].rows; + if (!rows) { + break; + } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layerId]); + for (const accessor of accessors) { + const hasArrayValues = rows.some((row) => Array.isArray(row[accessor])); + if (hasArrayValues) { + accessorsWithArrayValues.push(columnToLabel[accessor]); + } + } + } + return accessorsWithArrayValues.map((label) => ( + <> + <strong>{label}</strong> contains array values. Your visualization may not render as + expected. + </> + )); + }, }); function validateLayersForDimension( From 88359b742afd0e32acfc1ef64b1159d624714be5 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov <restrry@gmail.com> Date: Wed, 2 Dec 2020 22:15:25 +0300 Subject: [PATCH 068/107] Improve ui settings performance (#84513) * remove unused parameter in "read" function * add cache for uiSettings client * add tests for ui_settings client caching * address comments * do not mutate ui_settings_client cache --- src/core/server/ui_settings/cache.test.ts | 50 ++++++++ src/core/server/ui_settings/cache.ts | 48 ++++++++ .../ui_settings/ui_settings_client.test.ts | 107 ++++++++++++++++++ .../server/ui_settings/ui_settings_client.ts | 46 ++++---- 4 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 src/core/server/ui_settings/cache.test.ts create mode 100644 src/core/server/ui_settings/cache.ts diff --git a/src/core/server/ui_settings/cache.test.ts b/src/core/server/ui_settings/cache.test.ts new file mode 100644 index 0000000000000..ea375751fe437 --- /dev/null +++ b/src/core/server/ui_settings/cache.test.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Cache } from './cache'; + +describe('Cache', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + it('stores value for maxAge ms', async () => { + const cache = new Cache<number>(500); + cache.set(42); + expect(cache.get()).toBe(42); + jest.advanceTimersByTime(100); + expect(cache.get()).toBe(42); + }); + it('invalidates cache after maxAge ms', async () => { + const cache = new Cache<number>(500); + cache.set(42); + expect(cache.get()).toBe(42); + jest.advanceTimersByTime(1000); + expect(cache.get()).toBe(null); + }); + it('del invalidates cache immediately', async () => { + const cache = new Cache<number>(10); + cache.set(42); + expect(cache.get()).toBe(42); + cache.del(); + expect(cache.get()).toBe(null); + }); +}); diff --git a/src/core/server/ui_settings/cache.ts b/src/core/server/ui_settings/cache.ts new file mode 100644 index 0000000000000..697cf2b284c78 --- /dev/null +++ b/src/core/server/ui_settings/cache.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +const oneSec = 1000; +const defMaxAge = 5 * oneSec; +/** + * @internal + */ +export class Cache<T = Record<string, any>> { + private value: T | null; + private timer?: NodeJS.Timeout; + + /** + * Delete cached value after maxAge ms. + */ + constructor(private readonly maxAge: number = defMaxAge) { + this.value = null; + } + get() { + return this.value; + } + set(value: T) { + this.del(); + this.value = value; + this.timer = setTimeout(() => this.del(), this.maxAge); + } + del() { + if (this.timer) { + clearTimeout(this.timer); + } + this.value = null; + } +} diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index a38fb2ab7e06c..8238511e27ed9 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -676,4 +676,111 @@ describe('ui settings', () => { expect(uiSettings.isOverridden('bar')).toBe(true); }); }); + + describe('caching', () => { + describe('read operations cache user config', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('get', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('getAll', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.getAll(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.getAll(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + await uiSettings.getAll(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('getUserProvided', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.getUserProvided(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.getUserProvided(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + await uiSettings.getUserProvided(); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + }); + + describe('write operations invalidate user config cache', () => { + it('set', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.set('foo', 'bar'); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('setMany', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.setMany({ foo: 'bar' }); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('remove', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.remove('foo'); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + + it('removeMany', async () => { + const esDocSource = {}; + const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + + await uiSettings.get('any'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + + await uiSettings.removeMany(['foo', 'bar']); + await uiSettings.get('foo'); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + }); + }); + }); }); diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index f168784a93330..ab5fca9f81031 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -24,6 +24,7 @@ import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; import { CannotOverrideError } from './ui_settings_errors'; +import { Cache } from './cache'; export interface UiSettingsServiceOptions { type: string; @@ -36,7 +37,6 @@ export interface UiSettingsServiceOptions { } interface ReadOptions { - ignore401Errors?: boolean; autoCreateOrUpgradeIfMissing?: boolean; } @@ -58,6 +58,7 @@ export class UiSettingsClient implements IUiSettingsClient { private readonly overrides: NonNullable<UiSettingsServiceOptions['overrides']>; private readonly defaults: NonNullable<UiSettingsServiceOptions['defaults']>; private readonly log: Logger; + private readonly cache: Cache; constructor(options: UiSettingsServiceOptions) { const { type, id, buildNum, savedObjectsClient, log, defaults = {}, overrides = {} } = options; @@ -69,6 +70,7 @@ export class UiSettingsClient implements IUiSettingsClient { this.defaults = defaults; this.overrides = overrides; this.log = log; + this.cache = new Cache(); } getRegistered() { @@ -95,7 +97,12 @@ export class UiSettingsClient implements IUiSettingsClient { } async getUserProvided<T = unknown>(): Promise<UserProvided<T>> { - const userProvided: UserProvided<T> = this.onReadHook<T>(await this.read()); + const cachedValue = this.cache.get(); + if (cachedValue) { + return cachedValue; + } + + const userProvided: UserProvided<T> = this.onReadHook(await this.read()); // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object @@ -104,10 +111,13 @@ export class UiSettingsClient implements IUiSettingsClient { value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } + this.cache.set(userProvided); + return userProvided; } async setMany(changes: Record<string, any>) { + this.cache.del(); this.onWriteHook(changes); await this.write({ changes }); } @@ -140,7 +150,7 @@ export class UiSettingsClient implements IUiSettingsClient { private async getRaw(): Promise<UiSettingsRaw> { const userProvided = await this.getUserProvided(); - return defaultsDeep(userProvided, this.defaults); + return defaultsDeep({}, userProvided, this.defaults); } private validateKey(key: string, value: unknown) { @@ -209,10 +219,9 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private async read({ - ignore401Errors = false, - autoCreateOrUpgradeIfMissing = true, - }: ReadOptions = {}): Promise<Record<string, any>> { + private async read({ autoCreateOrUpgradeIfMissing = true }: ReadOptions = {}): Promise< + Record<string, any> + > { try { const resp = await this.savedObjectsClient.get<Record<string, any>>(this.type, this.id); return resp.attributes; @@ -227,16 +236,13 @@ export class UiSettingsClient implements IUiSettingsClient { }); if (!failedUpgradeAttributes) { - return await this.read({ - ignore401Errors, - autoCreateOrUpgradeIfMissing: false, - }); + return await this.read({ autoCreateOrUpgradeIfMissing: false }); } return failedUpgradeAttributes; } - if (this.isIgnorableError(error, ignore401Errors)) { + if (this.isIgnorableError(error)) { return {}; } @@ -244,17 +250,9 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private isIgnorableError(error: Error, ignore401Errors: boolean) { - const { - isForbiddenError, - isEsUnavailableError, - isNotAuthorizedError, - } = this.savedObjectsClient.errors; - - return ( - isForbiddenError(error) || - isEsUnavailableError(error) || - (ignore401Errors && isNotAuthorizedError(error)) - ); + private isIgnorableError(error: Error) { + const { isForbiddenError, isEsUnavailableError } = this.savedObjectsClient.errors; + + return isForbiddenError(error) || isEsUnavailableError(error); } } From b6913a3d2e5e3833ec391af4cb7dae511bc8ef21 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger <scotty.bollinger@elastic.co> Date: Wed, 2 Dec 2020 13:34:07 -0600 Subject: [PATCH 069/107] [Enterprise Search] Convert IndexingStatus to use logic for fetching (#84710) * Add IndexingStatusLogic * Replace IndexingStatusFetcher with logic * Refactor out unnecessary conditional onComplete is not optional so these if blocks can be consolidated * Misc styling - destructuring and typing Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Misc styling - imports Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Remove div * Refactor test * Replace method with string for statusPath In ent-search, we use Rails helpers to generate paths. These were in the form of routes.whateverPath(). We passed these method to the IndexingStatus component to generate the app-specific rotues in the shared component. In Kibana, we will not have these generators and should instead pass the path strings directly Co-authored-by: Constance <constancecchen@users.noreply.github.com> --- .../indexing_status/indexing_status.test.tsx | 37 ++++-- .../indexing_status/indexing_status.tsx | 57 +++++---- .../indexing_status_fetcher.tsx | 64 ---------- .../indexing_status_logic.test.ts | 110 ++++++++++++++++++ .../indexing_status/indexing_status_logic.ts | 78 +++++++++++++ 5 files changed, 247 insertions(+), 99 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx index 097c3bbc8e9ff..42cb6c229ad63 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../__mocks__/kea.mock'; +import '../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; @@ -11,41 +16,49 @@ import { EuiPanel } from '@elastic/eui'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; -import { IndexingStatusFetcher } from './indexing_status_fetcher'; import { IndexingStatus } from './indexing_status'; describe('IndexingStatus', () => { const getItemDetailPath = jest.fn(); - const getStatusPath = jest.fn(); const onComplete = jest.fn(); const setGlobalIndexingStatus = jest.fn(); + const fetchIndexingStatus = jest.fn(); const props = { percentageComplete: 50, numDocumentsWithErrors: 1, activeReindexJobId: 12, viewLinkPath: '/path', + statusPath: '/other_path', itemId: '1', getItemDetailPath, - getStatusPath, onComplete, setGlobalIndexingStatus, }; + beforeEach(() => { + setMockActions({ fetchIndexingStatus }); + }); + it('renders', () => { + setMockValues({ + percentageComplete: 50, + numDocumentsWithErrors: 0, + }); const wrapper = shallow(<IndexingStatus {...props} />); - const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')( - props.percentageComplete, - props.numDocumentsWithErrors - ); - expect(shallow(fetcher).find(EuiPanel)).toHaveLength(1); - expect(shallow(fetcher).find(IndexingStatusContent)).toHaveLength(1); + expect(wrapper.find(EuiPanel)).toHaveLength(1); + expect(wrapper.find(IndexingStatusContent)).toHaveLength(1); + expect(fetchIndexingStatus).toHaveBeenCalled(); }); it('renders errors', () => { - const wrapper = shallow(<IndexingStatus {...props} percentageComplete={100} />); - const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')(100, 1); - expect(shallow(fetcher).find(IndexingStatusErrors)).toHaveLength(1); + setMockValues({ + percentageComplete: 100, + numDocumentsWithErrors: 1, + }); + const wrapper = shallow(<IndexingStatus {...props} />); + + expect(wrapper.find(IndexingStatusErrors)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx index beec0babea590..b2109b7ef3f0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -4,41 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useValues, useActions } from 'kea'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; -import { IndexingStatusFetcher } from './indexing_status_fetcher'; +import { IndexingStatusLogic } from './indexing_status_logic'; import { IIndexingStatus } from '../types'; -export interface IIndexingStatusProps extends IIndexingStatus { +export interface IIndexingStatusProps { viewLinkPath: string; itemId: string; + statusPath: string; getItemDetailPath?(itemId: string): string; - getStatusPath(itemId: string, activeReindexJobId: number): string; onComplete(numDocumentsWithErrors: number): void; setGlobalIndexingStatus?(activeReindexJob: IIndexingStatus): void; } -export const IndexingStatus: React.FC<IIndexingStatusProps> = (props) => ( - <IndexingStatusFetcher {...props}> - {(percentageComplete, numDocumentsWithErrors) => ( - <div> - {percentageComplete < 100 && ( - <EuiPanel paddingSize="l" hasShadow> - <IndexingStatusContent percentageComplete={percentageComplete} /> - </EuiPanel> - )} - {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( - <> - <EuiSpacer /> - <IndexingStatusErrors viewLinkPath={props.viewLinkPath} /> - </> - )} - </div> - )} - </IndexingStatusFetcher> -); +export const IndexingStatus: React.FC<IIndexingStatusProps> = ({ + viewLinkPath, + statusPath, + onComplete, +}) => { + const { percentageComplete, numDocumentsWithErrors } = useValues(IndexingStatusLogic); + const { fetchIndexingStatus } = useActions(IndexingStatusLogic); + + useEffect(() => { + fetchIndexingStatus({ statusPath, onComplete }); + }, []); + + return ( + <> + {percentageComplete < 100 && ( + <EuiPanel paddingSize="l" hasShadow={true}> + <IndexingStatusContent percentageComplete={percentageComplete} /> + </EuiPanel> + )} + {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( + <> + <EuiSpacer /> + <IndexingStatusErrors viewLinkPath={viewLinkPath} /> + </> + )} + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx deleted file mode 100644 index cb7c82f91ed61..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect, useState, useRef } from 'react'; - -import { HttpLogic } from '../http'; -import { flashAPIErrors } from '../flash_messages'; - -interface IIndexingStatusFetcherProps { - activeReindexJobId: number; - itemId: string; - percentageComplete: number; - numDocumentsWithErrors: number; - onComplete?(numDocumentsWithErrors: number): void; - getStatusPath(itemId: string, activeReindexJobId: number): string; - children(percentageComplete: number, numDocumentsWithErrors: number): JSX.Element; -} - -export const IndexingStatusFetcher: React.FC<IIndexingStatusFetcherProps> = ({ - activeReindexJobId, - children, - getStatusPath, - itemId, - numDocumentsWithErrors, - onComplete, - percentageComplete = 0, -}) => { - const [indexingStatus, setIndexingStatus] = useState({ - numDocumentsWithErrors, - percentageComplete, - }); - const pollingInterval = useRef<number>(); - - useEffect(() => { - pollingInterval.current = window.setInterval(async () => { - try { - const response = await HttpLogic.values.http.get(getStatusPath(itemId, activeReindexJobId)); - if (response.percentageComplete >= 100) { - clearInterval(pollingInterval.current); - } - setIndexingStatus({ - percentageComplete: response.percentageComplete, - numDocumentsWithErrors: response.numDocumentsWithErrors, - }); - if (response.percentageComplete >= 100 && onComplete) { - onComplete(response.numDocumentsWithErrors); - } - } catch (e) { - flashAPIErrors(e); - } - }, 3000); - - return () => { - if (pollingInterval.current) { - clearInterval(pollingInterval.current); - } - }; - }, []); - - return children(indexingStatus.percentageComplete, indexingStatus.numDocumentsWithErrors); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts new file mode 100644 index 0000000000000..9fa5fe0f84bab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { resetContext } from 'kea'; + +jest.mock('../http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn() } }, + }, +})); +import { HttpLogic } from '../http'; + +jest.mock('../flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../flash_messages'; + +import { IndexingStatusLogic } from './indexing_status_logic'; + +describe('IndexingStatusLogic', () => { + let unmount: any; + + const mockStatusResponse = { + percentageComplete: 50, + numDocumentsWithErrors: 3, + activeReindexJobId: 1, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + unmount = IndexingStatusLogic.mount(); + }); + + it('has expected default values', () => { + expect(IndexingStatusLogic.values).toEqual({ + percentageComplete: 100, + numDocumentsWithErrors: 0, + }); + }); + + describe('setIndexingStatus', () => { + it('sets reducers', () => { + IndexingStatusLogic.actions.setIndexingStatus(mockStatusResponse); + + expect(IndexingStatusLogic.values.percentageComplete).toEqual( + mockStatusResponse.percentageComplete + ); + expect(IndexingStatusLogic.values.numDocumentsWithErrors).toEqual( + mockStatusResponse.numDocumentsWithErrors + ); + }); + }); + + describe('fetchIndexingStatus', () => { + jest.useFakeTimers(); + const statusPath = '/api/workplace_search/path/123'; + const onComplete = jest.fn(); + const TIMEOUT = 3000; + + it('calls API and sets values', async () => { + const setIndexingStatusSpy = jest.spyOn(IndexingStatusLogic.actions, 'setIndexingStatus'); + const promise = Promise.resolve(mockStatusResponse); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); + jest.advanceTimersByTime(TIMEOUT); + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith(statusPath); + await promise; + + expect(setIndexingStatusSpy).toHaveBeenCalledWith(mockStatusResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); + jest.advanceTimersByTime(TIMEOUT); + + try { + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + }); + + it('handles indexing complete state', async () => { + const promise = Promise.resolve({ ...mockStatusResponse, percentageComplete: 100 }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete }); + jest.advanceTimersByTime(TIMEOUT); + + await promise; + + expect(clearInterval).toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalledWith(mockStatusResponse.numDocumentsWithErrors); + }); + + it('handles unmounting', async () => { + unmount(); + expect(clearInterval).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts new file mode 100644 index 0000000000000..cb484f19c157f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../http'; +import { IIndexingStatus } from '../types'; +import { flashAPIErrors } from '../flash_messages'; + +interface IndexingStatusProps { + statusPath: string; + onComplete(numDocumentsWithErrors: number): void; +} + +interface IndexingStatusActions { + fetchIndexingStatus(props: IndexingStatusProps): IndexingStatusProps; + setIndexingStatus({ + percentageComplete, + numDocumentsWithErrors, + }: IIndexingStatus): IIndexingStatus; +} + +interface IndexingStatusValues { + percentageComplete: number; + numDocumentsWithErrors: number; +} + +let pollingInterval: number; + +export const IndexingStatusLogic = kea<MakeLogicType<IndexingStatusValues, IndexingStatusActions>>({ + actions: { + fetchIndexingStatus: ({ statusPath, onComplete }) => ({ statusPath, onComplete }), + setIndexingStatus: ({ numDocumentsWithErrors, percentageComplete }) => ({ + numDocumentsWithErrors, + percentageComplete, + }), + }, + reducers: { + percentageComplete: [ + 100, + { + setIndexingStatus: (_, { percentageComplete }) => percentageComplete, + }, + ], + numDocumentsWithErrors: [ + 0, + { + setIndexingStatus: (_, { numDocumentsWithErrors }) => numDocumentsWithErrors, + }, + ], + }, + listeners: ({ actions }) => ({ + fetchIndexingStatus: ({ statusPath, onComplete }: IndexingStatusProps) => { + const { http } = HttpLogic.values; + + pollingInterval = window.setInterval(async () => { + try { + const response: IIndexingStatus = await http.get(statusPath); + if (response.percentageComplete >= 100) { + clearInterval(pollingInterval); + onComplete(response.numDocumentsWithErrors); + } + actions.setIndexingStatus(response); + } catch (e) { + flashAPIErrors(e); + } + }, 3000); + }, + }), + events: () => ({ + beforeUnmount() { + clearInterval(pollingInterval); + }, + }), +}); From 0e43beed4ffb5a2e0e0d5826f79b0b510be3364e Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 2 Dec 2020 13:35:38 -0600 Subject: [PATCH 070/107] [ML] Fix prediction probability text for classification (#84593) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../decision_path_chart.tsx | 31 ++++++++++++------- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx index 6bfd7a66331df..38eb7abc16814 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -91,8 +91,7 @@ export const DecisionPathChart = ({ maxDomain, baseline, }: DecisionPathChartProps) => { - // adjust the height so it's compact for items with more features - const baselineData: LineAnnotationDatum[] | undefined = useMemo( + const regressionBaselineData: LineAnnotationDatum[] | undefined = useMemo( () => baseline && isRegressionFeatureImportanceBaseline(baseline) ? [ @@ -111,6 +110,19 @@ export const DecisionPathChart = ({ : undefined, [baseline] ); + const xAxisLabel = regressionBaselineData + ? i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.decisionPathLinePredictionTitle', + { + defaultMessage: 'Prediction', + } + ) + : i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.decisionPathLinePredictionProbabilityTitle', + { + defaultMessage: 'Prediction probability', + } + ); // if regression, guarantee up to num_precision significant digits without having it in scientific notation // if classification, hide the numeric values since we only want to show the path const tickFormatter = useCallback((d) => formatSingleValue(d, '').toString(), []); @@ -121,11 +133,11 @@ export const DecisionPathChart = ({ size={{ height: DECISION_PATH_MARGIN + decisionPathData.length * DECISION_PATH_ROW_HEIGHT }} > <Settings theme={theme} rotation={90} /> - {baselineData && ( + {regressionBaselineData && ( <LineAnnotation id="xpack.ml.dataframe.analytics.explorationResults.decisionPathBaseline" domainType={AnnotationDomainTypes.YDomain} - dataValues={baselineData} + dataValues={regressionBaselineData} style={baselineStyle} marker={AnnotationBaselineMarker} /> @@ -137,8 +149,8 @@ export const DecisionPathChart = ({ title={i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle', { - defaultMessage: "Prediction for '{predictionFieldName}'", - values: { predictionFieldName }, + defaultMessage: "{xAxisLabel} for '{predictionFieldName}'", + values: { predictionFieldName, xAxisLabel }, } )} showGridLines={false} @@ -156,12 +168,7 @@ export const DecisionPathChart = ({ <Axis showGridLines={true} id="left" position={Position.Left} /> <LineSeries id={'xpack.ml.dataframe.analytics.explorationResults.decisionPathLine'} - name={i18n.translate( - 'xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle', - { - defaultMessage: 'Prediction', - } - )} + name={xAxisLabel} xScaleType={ScaleType.Ordinal} yScaleType={ScaleType.Linear} xAccessor={0} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4991c4d16099c..7b84c62264c83 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12012,10 +12012,8 @@ "xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle": "クラス名", "xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText": "ベースライン(学習データセットのすべてのデータポイントの予測の平均)", "xpack.ml.dataframe.analytics.explorationResults.decisionPathJSONTab": "JSON", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle": "予測", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotHelpText": "SHAP決定プロットは{linkedFeatureImportanceValues}を使用して、モデルがどのように「{predictionFieldName}」の予測値に到達するのかを示します。", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotTab": "決定プロット", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle": "「{predictionFieldName}」の予測", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.explorationResults.linkedFeatureImportanceValues": "特徴量の重要度値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 59ab890cfd9db..55071303a1b36 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12025,10 +12025,8 @@ "xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle": "类名称", "xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText": "基线(训练数据集中所有数据点的预测平均值)", "xpack.ml.dataframe.analytics.explorationResults.decisionPathJSONTab": "JSON", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathLineTitle": "预测", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotHelpText": "SHAP 决策图使用 {linkedFeatureImportanceValues} 说明模型如何达到“{predictionFieldName}”的预测值。", "xpack.ml.dataframe.analytics.explorationResults.decisionPathPlotTab": "决策图", - "xpack.ml.dataframe.analytics.explorationResults.decisionPathXAxisTitle": "“{predictionFieldName}”的预测", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.explorationResults.linkedFeatureImportanceValues": "特征重要性值", From 2ffdf75b6e93730bc347e0e57a6d2f5b5830053c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:37:36 -0500 Subject: [PATCH 071/107] [SECURITY_SOLUTION] Enable usage of the Endpoint Policy form from Fleet (#84684) * Endpoint: add `withSecurityContext` HOC + refactor endpoint policy edit lazy component to use it * Endpoint: refactor Policy Details to separate form from view * Endpoint: Enable the Redux store for the Policy form when displayed via Fleet * Fleet: Allow partial package policy updates to be sent via `onChange()` --- .../step_define_package_policy.tsx | 1 + .../edit_package_policy_page/index.tsx | 2 + .../applications/fleet/types/ui_extensions.ts | 8 +- .../view/components/config_form/index.tsx | 4 +- .../endpoint_policy_edit_extension.tsx | 102 +++++++++++++----- .../lazy_endpoint_policy_edit_extension.tsx | 31 ++++-- .../with_security_context.tsx | 64 +++++++++++ .../pages/policy/view/policy_details.tsx | 60 ++--------- .../pages/policy/view/policy_details_form.tsx | 68 ++++++++++++ .../security_solution/public/plugin.tsx | 4 +- .../apps/endpoint/policy_details.ts | 70 ++++++++---- .../apps/endpoint/policy_list.ts | 2 +- ...gest_manager_create_package_policy_page.ts | 29 +++-- 13 files changed, 323 insertions(+), 122 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 9f687b39c094e..f6533a06cea27 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -143,6 +143,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ description: e.target.value, }) } + data-test-subj="packagePolicyDescriptionInput" /> </EuiFormRow> <EuiSpacer size="m" /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 425d1435a716e..8f798445b2362 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -244,6 +244,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { packagePolicyName: packagePolicy.name, }, }), + 'data-test-subj': 'policyUpdateSuccessToast', text: agentCount && agentPolicy ? i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationMessage', { @@ -406,6 +407,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { iconType="save" color="primary" fill + data-test-subj="saveIntegration" > <FormattedMessage id="xpack.fleet.editPackagePolicy.saveButton" diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts index a8a961ca949b6..d35e5f4744449 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts @@ -28,13 +28,17 @@ export interface PackagePolicyEditExtensionComponentProps { newPolicy: NewPackagePolicy; /** * A callback that should be executed anytime a change to the Integration Policy needs to - * be reported back to the Fleet Policy Edit page + * be reported back to the Fleet Policy Edit page. + * + * **NOTE:** + * this callback will be recreated everytime the policy data changes, thus logic around its + * invocation should take that into consideration in order to avoid an endless loop. */ onChange: (opts: { /** is current form state is valid */ isValid: boolean; /** The updated Integration Policy to be merged back and included in the API call */ - updatedPolicy: NewPackagePolicy; + updatedPolicy: Partial<NewPackagePolicy>; }) => void; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx index 30c35de9b907f..ce5eb03d60cd0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -58,12 +58,12 @@ export const ConfigForm: FC<ConfigFormProps> = memo( <ConfigFormHeading>{TITLES.type}</ConfigFormHeading> <EuiText size="m">{type}</EuiText> </EuiFlexItem> - <EuiFlexItem grow={2}> + <EuiFlexItem> <ConfigFormHeading>{TITLES.os}</ConfigFormHeading> <EuiText>{supportedOss.map((os) => OS_TITLES[os]).join(', ')}</EuiText> </EuiFlexItem> <EuiShowFor sizes={['m', 'l', 'xl']}> - <EuiFlexItem> + <EuiFlexItem grow={2}> <EuiFlexGroup direction="row" gutterSize="none" justifyContent="flexEnd"> <EuiFlexItem grow={false}>{rightCorner}</EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 95bf23b532f41..6d464280b2763 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, @@ -19,9 +19,11 @@ import { EuiContextMenuPanelProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useDispatch } from 'react-redux'; import { pagePathGetters, PackagePolicyEditExtensionComponentProps, + NewPackagePolicy, } from '../../../../../../../fleet/public'; import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../common/routing'; import { MANAGEMENT_APP_ID } from '../../../../common/constants'; @@ -31,13 +33,17 @@ import { } from '../../../../../../common/endpoint/types'; import { useKibana } from '../../../../../common/lib/kibana'; import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { PolicyDetailsForm } from '../policy_details_form'; +import { AppAction } from '../../../../../common/store/actions'; +import { usePolicyDetailsSelector } from '../policy_hooks'; +import { policyDetailsForUpdate } from '../../store/policy_details/selectors'; /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy */ export const EndpointPolicyEditExtension = memo<PackagePolicyEditExtensionComponentProps>( - ({ policy }) => { + ({ policy, onChange }) => { return ( <> <EuiSpacer size="m" /> @@ -46,12 +52,81 @@ export const EndpointPolicyEditExtension = memo<PackagePolicyEditExtensionCompon <EditFlowMessage agentPolicyId={policy.policy_id} integrationPolicyId={policy.id} /> </EuiText> </EuiCallOut> + <EuiSpacer size="m" /> + <WrappedPolicyDetailsForm policyId={policy.id} onChange={onChange} /> </> ); } ); EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension'; +const WrappedPolicyDetailsForm = memo<{ + policyId: string; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; +}>(({ policyId, onChange }) => { + const dispatch = useDispatch<(a: AppAction) => void>(); + const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); + const [, setLastUpdatedPolicy] = useState(updatedPolicy); + + // When the form is initially displayed, trigger the Redux middleware which is based on + // the location information stored via the `userChangedUrl` action. + useEffect(() => { + dispatch({ + type: 'userChangedUrl', + payload: { + hash: '', + pathname: getPolicyDetailPath(policyId, ''), + search: '', + }, + }); + + // When form is unloaded, reset the redux store + return () => { + dispatch({ + type: 'userChangedUrl', + payload: { + hash: '', + pathname: '/', + search: '', + }, + }); + }; + }, [dispatch, policyId]); + + useEffect(() => { + // Currently, the `onChange` callback provided by the fleet UI extension is regenerated every + // time the policy data is updated, which means this will go into a continious loop if we don't + // actually check to see if an update should be reported back to fleet + setLastUpdatedPolicy((prevState) => { + if (prevState === updatedPolicy) { + return prevState; + } + + if (updatedPolicy) { + onChange({ + isValid: true, + // send up only the updated policy data which is stored in the `inputs` section. + // All other attributes (like name, id) are updated from the Fleet form, so we want to + // ensure we don't override it. + updatedPolicy: { + // Casting is needed due to the use of `Immutable<>` in our store data + inputs: (updatedPolicy.inputs as unknown) as NewPackagePolicy['inputs'], + }, + }); + } + + return updatedPolicy; + }); + }, [onChange, updatedPolicy]); + + return ( + <div data-test-subj="endpointIntegrationPolicyForm"> + <PolicyDetailsForm /> + </div> + ); +}); +WrappedPolicyDetailsForm.displayName = 'WrappedPolicyDetailsForm'; + const EditFlowMessage = memo<{ agentPolicyId: string; integrationPolicyId: string; @@ -82,17 +157,6 @@ const EditFlowMessage = memo<{ const handleClosePopup = useCallback(() => setIsMenuOpen(false), []); - const handleSecurityPolicyAction = useNavigateToAppEventHandler<PolicyDetailsRouteState>( - MANAGEMENT_APP_ID, - { - path: getPolicyDetailPath(integrationPolicyId), - state: { - onSaveNavigateTo: navigateBackToIngest, - onCancelNavigateTo: navigateBackToIngest, - }, - } - ); - const handleTrustedAppsAction = useNavigateToAppEventHandler<TrustedAppsListPageRouteState>( MANAGEMENT_APP_ID, { @@ -129,16 +193,6 @@ const EditFlowMessage = memo<{ const actionItems = useMemo<EuiContextMenuPanelProps['items']>(() => { return [ - <EuiContextMenuItem - key="policyDetails" - onClick={handleSecurityPolicyAction} - data-test-subj="securityPolicy" - > - <FormattedMessage - id="xpack.securitySolution.endpoint.fleet.editPackagePolicy.actionSecurityPolicy" - defaultMessage="Edit Policy" - /> - </EuiContextMenuItem>, <EuiContextMenuItem key="trustedApps" onClick={handleTrustedAppsAction} @@ -150,7 +204,7 @@ const EditFlowMessage = memo<{ /> </EuiContextMenuItem>, ]; - }, [handleSecurityPolicyAction, handleTrustedAppsAction]); + }, [handleTrustedAppsAction]); return ( <EuiFlexGroup> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx index b417bc9ad5d9c..78a83fa11ae3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx @@ -5,14 +5,29 @@ */ import { lazy } from 'react'; -import { PackagePolicyEditExtensionComponent } from '../../../../../../../fleet/public'; +import { CoreStart } from 'kibana/public'; +import { + PackagePolicyEditExtensionComponent, + PackagePolicyEditExtensionComponentProps, +} from '../../../../../../../fleet/public'; +import { StartPlugins } from '../../../../../types'; + +export const getLazyEndpointPolicyEditExtension = ( + coreStart: CoreStart, + depsStart: Pick<StartPlugins, 'data' | 'fleet'> +) => { + return lazy<PackagePolicyEditExtensionComponent>(async () => { + const [{ withSecurityContext }, { EndpointPolicyEditExtension }] = await Promise.all([ + import('./with_security_context'), + import('./endpoint_policy_edit_extension'), + ]); -export const LazyEndpointPolicyEditExtension = lazy<PackagePolicyEditExtensionComponent>( - async () => { - const { EndpointPolicyEditExtension } = await import('./endpoint_policy_edit_extension'); return { - // FIXME: remove casting once old UI component registration is removed - default: (EndpointPolicyEditExtension as unknown) as PackagePolicyEditExtensionComponent, + default: withSecurityContext<PackagePolicyEditExtensionComponentProps>({ + coreStart, + depsStart, + WrappedComponent: EndpointPolicyEditExtension, + }), }; - } -); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx new file mode 100644 index 0000000000000..f65dbaf1087d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ComponentType, memo } from 'react'; +import { CoreStart } from 'kibana/public'; +import { combineReducers, createStore, compose, applyMiddleware } from 'redux'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { StartPlugins } from '../../../../../types'; +import { managementReducer } from '../../../../store/reducer'; +import { managementMiddlewareFactory } from '../../../../store/middleware'; + +type ComposeType = typeof compose; +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ComposeType; + } +} +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +interface WithSecurityContextProps<P extends {}> { + coreStart: CoreStart; + depsStart: Pick<StartPlugins, 'data' | 'fleet'>; + WrappedComponent: ComponentType<P>; +} + +/** + * Returns a new component that wraps the provided `WrappedComponent` in a bare minimum set of rendering context + * needed to render Security Solution components that may be dependent on a Redux store and/or Security Solution + * specific context based functionality + * + * @param coreStart + * @param depsStart + * @param WrappedComponent + */ +export const withSecurityContext = <P extends {}>({ + coreStart, + depsStart, + WrappedComponent, +}: WithSecurityContextProps<P>): ComponentType<P> => { + let store: ReturnType<typeof createStore>; // created on first render + + return memo((props) => { + if (!store) { + // Most of the code here was copied form + // x-pack/plugins/security_solution/public/management/index.ts + store = createStore( + combineReducers({ + management: managementReducer, + }), + { management: undefined }, + composeEnhancers(applyMiddleware(...managementMiddlewareFactory(coreStart, depsStart))) + ); + } + + return ( + <ReduxStoreProvider store={store}> + <WrappedComponent {...props} /> + </ReduxStoreProvider> + ); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 1ce099c494cf0..666e27c9d9a26 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -10,7 +10,6 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, - EuiText, EuiSpacer, EuiOverlayMask, EuiConfirmModal, @@ -34,9 +33,6 @@ import { import { useKibana, toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; -import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; -import { MalwareProtections } from './policy_forms/protections/malware'; -import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -48,7 +44,7 @@ import { MANAGEMENT_APP_ID } from '../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; import { WrapperPage } from '../../../../common/components/wrapper_page'; import { HeaderPage } from '../../../../common/components/header_page'; -import { AdvancedPolicyForms } from './policy_advanced'; +import { PolicyDetailsForm } from './policy_details_form'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -71,7 +67,6 @@ export const PolicyDetails = React.memo(() => { // Local state const [showConfirm, setShowConfirm] = useState<boolean>(false); const [routeState, setRouteState] = useState<PolicyDetailsRouteState>(); - const [showAdvancedPolicy, setShowAdvancedPolicy] = useState<boolean>(false); const policyName = policyItem?.name ?? ''; const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); @@ -111,9 +106,11 @@ export const PolicyDetails = React.memo(() => { } }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState]); + const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; const navigateToAppArguments = useMemo((): Parameters<ApplicationStart['navigateToApp']> => { - return routeState?.onCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: hostListRouterPath }]; - }, [hostListRouterPath, routeState?.onCancelNavigateTo]); + return routingOnCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: hostListRouterPath }]; + }, [hostListRouterPath, routingOnCancelNavigateTo]); + const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); const handleSaveOnClick = useCallback(() => { @@ -131,10 +128,6 @@ export const PolicyDetails = React.memo(() => { setShowConfirm(false); }, []); - const handleAdvancedPolicyClick = useCallback(() => { - setShowAdvancedPolicy(!showAdvancedPolicy); - }, [showAdvancedPolicy]); - useEffect(() => { if (!routeState && locationRouteState) { setRouteState(locationRouteState); @@ -224,48 +217,7 @@ export const PolicyDetails = React.memo(() => { {headerRightContent} </HeaderPage> - <EuiText size="xs" color="subdued"> - <h4> - <FormattedMessage - id="xpack.securitySolution.endpoint.policy.details.protections" - defaultMessage="Protections" - /> - </h4> - </EuiText> - - <EuiSpacer size="xs" /> - <MalwareProtections /> - <EuiSpacer size="l" /> - - <EuiText size="xs" color="subdued"> - <h4> - <FormattedMessage - id="xpack.securitySolution.endpoint.policy.details.settings" - defaultMessage="Settings" - /> - </h4> - </EuiText> - - <EuiSpacer size="xs" /> - <WindowsEvents /> - <EuiSpacer size="l" /> - <MacEvents /> - <EuiSpacer size="l" /> - <LinuxEvents /> - <EuiSpacer size="l" /> - <AntivirusRegistrationForm /> - - <EuiSpacer size="l" /> - <EuiButtonEmpty data-test-subj="advancedPolicyButton" onClick={handleAdvancedPolicyClick}> - <FormattedMessage - id="xpack.securitySolution.endpoint.policy.advanced.show" - defaultMessage="{action} advanced settings" - values={{ action: showAdvancedPolicy ? 'Hide' : 'Show' }} - /> - </EuiButtonEmpty> - - <EuiSpacer size="l" /> - {showAdvancedPolicy && <AdvancedPolicyForms />} + <PolicyDetailsForm /> </WrapperPage> <SpyRoute pageName={SecurityPageName.administration} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx new file mode 100644 index 0000000000000..a0bf2b37e8a12 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MalwareProtections } from './policy_forms/protections/malware'; +import { LinuxEvents, MacEvents, WindowsEvents } from './policy_forms/events'; +import { AdvancedPolicyForms } from './policy_advanced'; +import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; + +export const PolicyDetailsForm = memo(() => { + const [showAdvancedPolicy, setShowAdvancedPolicy] = useState<boolean>(false); + const handleAdvancedPolicyClick = useCallback(() => { + setShowAdvancedPolicy(!showAdvancedPolicy); + }, [showAdvancedPolicy]); + + return ( + <> + <EuiText size="xs" color="subdued"> + <h4> + <FormattedMessage + id="xpack.securitySolution.endpoint.policy.details.protections" + defaultMessage="Protections" + /> + </h4> + </EuiText> + + <EuiSpacer size="xs" /> + <MalwareProtections /> + <EuiSpacer size="l" /> + + <EuiText size="xs" color="subdued"> + <h4> + <FormattedMessage + id="xpack.securitySolution.endpoint.policy.details.settings" + defaultMessage="Settings" + /> + </h4> + </EuiText> + + <EuiSpacer size="xs" /> + <WindowsEvents /> + <EuiSpacer size="l" /> + <MacEvents /> + <EuiSpacer size="l" /> + <LinuxEvents /> + <EuiSpacer size="l" /> + <AntivirusRegistrationForm /> + + <EuiSpacer size="l" /> + <EuiButtonEmpty data-test-subj="advancedPolicyButton" onClick={handleAdvancedPolicyClick}> + <FormattedMessage + id="xpack.securitySolution.endpoint.policy.advanced.show" + defaultMessage="{action} advanced settings" + values={{ action: showAdvancedPolicy ? 'Hide' : 'Show' }} + /> + </EuiButtonEmpty> + + <EuiSpacer size="l" /> + {showAdvancedPolicy && <AdvancedPolicyForms />} + </> + ); +}); +PolicyDetailsForm.displayName = 'PolicyDetailsForm'; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b81be3249953e..4f37b5b15d73a 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -61,7 +61,7 @@ import { import { SecurityAppStore } from './common/store/store'; import { getCaseConnectorUI } from './cases/components/connectors'; import { licenseService } from './common/hooks/use_license'; -import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; +import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { @@ -337,7 +337,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S registerExtension({ package: 'endpoint', view: 'package-policy-edit', - component: LazyEndpointPolicyEditExtension, + component: getLazyEndpointPolicyEditExtension(core, plugins), }); registerExtension({ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 46085b0db3063..166fc39f4aaaa 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -281,7 +281,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await actionsButton.click(); const menuPanel = await testSubjects.find('endpointActionsMenuPanel'); const actionItems = await menuPanel.findAllByTagName<'button'>('button'); - const expectedItems = ['Edit Policy', 'Edit Trusted Applications']; + const expectedItems = ['Edit Trusted Applications']; for (const action of actionItems) { const buttonText = await action.getVisibleText(); @@ -289,27 +289,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } }); - it('should navigate to Policy Details when the edit security policy action is clicked', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy'); - await pageObjects.policy.ensureIsOnDetailsPage(); - }); - - it('should allow the user to navigate, edit, save Policy Details and be redirected back to ingest', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy'); - await pageObjects.policy.ensureIsOnDetailsPage(); - await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - await pageObjects.policy.confirmAndSave(); - - await testSubjects.existOrFail('policyDetailsSuccessMessage'); - await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); - }); - - it('should navigate back to Ingest Policy Edit package page on click of cancel button', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy'); - await (await pageObjects.policy.findCancelButton()).click(); - await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); - }); - it('should navigate to Trusted Apps', async () => { await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('trustedApps'); await pageObjects.trustedApps.ensureIsOnTrustedAppsListPage(); @@ -321,6 +300,53 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await backButton.click(); await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); }); + + it('should show the endpoint policy form', async () => { + await testSubjects.existOrFail('endpointIntegrationPolicyForm'); + }); + + it('should allow updates to policy items', async () => { + const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( + winDnsEventingCheckbox + ); + expect(await winDnsEventingCheckbox.isSelected()).to.be(true); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + expect(await winDnsEventingCheckbox.isSelected()).to.be(false); + }); + + it('should preserve updates done from the Fleet form', async () => { + await pageObjects.ingestManagerCreatePackagePolicy.setPackagePolicyDescription( + 'protect everything' + ); + + const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( + winDnsEventingCheckbox + ); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + + expect( + await pageObjects.ingestManagerCreatePackagePolicy.getPackagePolicyDescriptionValue() + ).to.be('protect everything'); + }); + + it('should include updated endpoint data when saved', async () => { + const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( + winDnsEventingCheckbox + ); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + const wasSelected = await winDnsEventingCheckbox.isSelected(); + await (await pageObjects.ingestManagerCreatePackagePolicy.findSaveButton(true)).click(); + await pageObjects.ingestManagerCreatePackagePolicy.waitForSaveSuccessNotification(true); + + await pageObjects.ingestManagerCreatePackagePolicy.navigateToAgentPolicyEditPackagePolicy( + policyInfo.agentPolicy.id, + policyInfo.packagePolicy.id + ); + expect(await testSubjects.isSelected('policyWindowsEvent_dns')).to.be(wasSelected); + }); }); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 70958d7ca7631..741040b12fd7b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -140,7 +140,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const newPolicyName = `endpoint policy ${Date.now()}`; await pageObjects.ingestManagerCreatePackagePolicy.selectAgentPolicy(); await pageObjects.ingestManagerCreatePackagePolicy.setPackagePolicyName(newPolicyName); - await (await pageObjects.ingestManagerCreatePackagePolicy.findDSaveButton()).click(); + await (await pageObjects.ingestManagerCreatePackagePolicy.findSaveButton()).click(); await pageObjects.ingestManagerCreatePackagePolicy.waitForSaveSuccessNotification(); await pageObjects.policy.ensureIsOnPolicyPage(); await policyTestResources.deletePolicyByName(newPolicyName); diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts index 747b62a9550c6..48e5b6a23458f 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts @@ -41,8 +41,10 @@ export function IngestManagerCreatePackagePolicy({ /** * Finds and returns the save button on the sticky bottom bar */ - async findDSaveButton() { - return await testSubjects.find('createPackagePolicySaveButton'); + async findSaveButton(forEditPage: boolean = false) { + return await testSubjects.find( + forEditPage ? 'saveIntegration' : 'createPackagePolicySaveButton' + ); }, /** @@ -80,11 +82,22 @@ export function IngestManagerCreatePackagePolicy({ await testSubjects.setValue('packagePolicyNameInput', name); }, + async getPackagePolicyDescriptionValue() { + return await testSubjects.getAttribute('packagePolicyDescriptionInput', 'value'); + }, + + async setPackagePolicyDescription(desc: string) { + await this.scrollToCenterOfWindow('packagePolicyDescriptionInput'); + await testSubjects.setValue('packagePolicyDescriptionInput', desc); + }, + /** * Waits for the save Notification toast to be visible */ - async waitForSaveSuccessNotification() { - await testSubjects.existOrFail('packagePolicyCreateSuccessToast'); + async waitForSaveSuccessNotification(forEditPage: boolean = false) { + await testSubjects.existOrFail( + forEditPage ? 'policyUpdateSuccessToast' : 'packagePolicyCreateSuccessToast' + ); }, /** @@ -115,11 +128,13 @@ export function IngestManagerCreatePackagePolicy({ /** * Center a given Element on the Window viewport - * @param element + * @param element if defined as a string, it should be the test subject to find */ - async scrollToCenterOfWindow(element: WebElementWrapper) { + async scrollToCenterOfWindow(element: WebElementWrapper | string) { + const ele = typeof element === 'string' ? await testSubjects.find(element) : element; + const [elementPosition, windowSize] = await Promise.all([ - element.getPosition(), + ele.getPosition(), browser.getWindowSize(), ]); await browser.execute( From b593781009c47b4f5cf8d5010d571d55f6b2dcbc Mon Sep 17 00:00:00 2001 From: Tyler Smalley <tyler.smalley@elastic.co> Date: Wed, 2 Dec 2020 11:42:23 -0800 Subject: [PATCH 072/107] Jest multi-project configuration (#77894) Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co> --- .../development-unit-tests.asciidoc | 6 +- ...tegration.js => jest.config.integration.js | 16 +- jest.config.js | 23 + jest.config.oss.js | 30 + packages/README.md | 2 +- packages/kbn-apm-config-loader/jest.config.js | 24 + packages/kbn-babel-code-parser/jest.config.js | 24 + packages/kbn-config-schema/jest.config.js | 24 + packages/kbn-config/jest.config.js | 24 + packages/kbn-dev-utils/jest.config.js | 24 + packages/kbn-es-archiver/jest.config.js | 24 + packages/kbn-es/jest.config.js | 24 + packages/kbn-i18n/jest.config.js | 25 + packages/kbn-interpreter/jest.config.js | 24 + packages/kbn-legacy-logging/jest.config.js | 24 + packages/kbn-logging/jest.config.js | 24 + packages/kbn-monaco/jest.config.js | 24 + packages/kbn-optimizer/jest.config.js | 24 + packages/kbn-plugin-generator/jest.config.js | 24 + packages/kbn-pm/jest.config.js | 24 + packages/kbn-release-notes/jest.config.js | 24 + packages/kbn-spec-to-console/jest.config.js | 24 + packages/kbn-std/jest.config.js | 24 + packages/kbn-telemetry-tools/jest.config.js | 24 + packages/kbn-test/jest.config.js | 24 + packages/kbn-test/src/index.ts | 2 + .../src/jest/run_check_jest_configs_cli.ts | 113 + packages/kbn-ui-framework/jest.config.js | 24 + packages/kbn-utils/jest.config.js | 24 + .../cli.js => scripts/check_jest_configs.js | 5 +- scripts/jest.js | 15 +- scripts/jest_integration.js | 16 +- src/cli/jest.config.js | 24 + src/cli_encryption_keys/jest.config.js | 24 + src/cli_keystore/jest.config.js | 24 + src/cli_plugin/jest.config.js | 24 + src/core/jest.config.js | 25 + .../integration_tests/team_assignment.test.js | 3 +- src/dev/jest.config.js | 24 + src/legacy/server/jest.config.js | 24 + src/legacy/ui/jest.config.js | 24 + src/legacy/utils/jest.config.js | 24 + src/optimize/jest.config.js | 24 + src/plugins/advanced_settings/jest.config.js | 24 + src/plugins/bfetch/jest.config.js | 24 + src/plugins/charts/jest.config.js | 24 + .../console/jest.config.js} | 21 +- src/plugins/dashboard/jest.config.js | 25 + src/plugins/data/jest.config.js | 25 + src/plugins/discover/jest.config.js | 25 + src/plugins/embeddable/jest.config.js | 25 + src/plugins/es_ui_shared/jest.config.js | 24 + src/plugins/expressions/jest.config.js | 24 + src/plugins/home/jest.config.js | 24 + .../index_pattern_management/jest.config.js | 25 + src/plugins/input_control_vis/jest.config.js | 24 + src/plugins/inspector/jest.config.js | 24 + src/plugins/kibana_legacy/jest.config.js | 24 + src/plugins/kibana_overview/jest.config.js | 24 + src/plugins/kibana_react/jest.config.js | 24 + .../kibana_usage_collection/jest.config.js | 24 + src/plugins/kibana_utils/jest.config.js | 24 + src/plugins/legacy_export/jest.config.js | 24 + src/plugins/management/jest.config.js | 24 + src/plugins/maps_legacy/jest.config.js | 24 + src/plugins/navigation/jest.config.js | 24 + src/plugins/newsfeed/jest.config.js | 24 + src/plugins/region_map/jest.config.js | 24 + src/plugins/saved_objects/jest.config.js | 24 + .../saved_objects_management/jest.config.js | 24 + .../saved_objects_tagging_oss/jest.config.js | 24 + src/plugins/security_oss/jest.config.js | 24 + src/plugins/share/jest.config.js | 24 + src/plugins/telemetry/jest.config.js | 24 + .../jest.config.js | 24 + .../jest.config.js | 24 + src/plugins/tile_map/jest.config.js | 24 + src/plugins/ui_actions/jest.config.js | 24 + src/plugins/url_forwarding/jest.config.js | 24 + src/plugins/usage_collection/jest.config.js | 24 + src/plugins/vis_default_editor/jest.config.js | 24 + src/plugins/vis_type_markdown/jest.config.js | 24 + src/plugins/vis_type_metric/jest.config.js | 24 + src/plugins/vis_type_table/jest.config.js | 25 + src/plugins/vis_type_tagcloud/jest.config.js | 25 + src/plugins/vis_type_timelion/jest.config.js | 24 + .../vis_type_timeseries/jest.config.js | 24 + src/plugins/vis_type_vega/jest.config.js | 24 + src/plugins/vis_type_vislib/jest.config.js | 24 + src/plugins/visualizations/jest.config.js | 24 + src/plugins/visualize/jest.config.js | 24 + src/setup_node_env/jest.config.js | 24 + src/test_utils/jest.config.js | 24 + test/functional/jest.config.js | 24 + test/scripts/checks/jest_configs.sh | 5 + vars/tasks.groovy | 1 + x-pack/dev-tools/jest/create_jest_config.js | 22 - x-pack/dev-tools/jest/index.js | 25 - x-pack/jest.config.js | 14 + x-pack/plugins/actions/jest.config.js | 11 + .../plugins/alerting_builtins/jest.config.js | 11 + x-pack/plugins/alerts/jest.config.js | 11 + x-pack/plugins/apm/jest.config.js | 32 +- x-pack/plugins/audit_trail/jest.config.js | 11 + .../plugins/beats_management/jest.config.js | 11 + x-pack/plugins/canvas/jest.config.js | 11 + x-pack/plugins/canvas/scripts/jest.js | 108 +- .../plugins/canvas/storybook/dll_contexts.js | 3 - x-pack/plugins/case/jest.config.js | 11 + x-pack/plugins/cloud/jest.config.js | 11 + x-pack/plugins/code/jest.config.js | 11 + .../cross_cluster_replication/jest.config.js | 11 + .../plugins/dashboard_enhanced/jest.config.js | 11 + x-pack/plugins/dashboard_mode/jest.config.js | 11 + x-pack/plugins/data_enhanced/jest.config.js | 11 + .../plugins/discover_enhanced/jest.config.js | 11 + x-pack/plugins/drilldowns/jest.config.js | 11 + .../embeddable_enhanced/jest.config.js | 11 + .../encrypted_saved_objects/jest.config.js | 11 + .../plugins/enterprise_search/jest.config.js | 11 + x-pack/plugins/event_log/jest.config.js | 11 + x-pack/plugins/features/jest.config.js | 11 + x-pack/plugins/file_upload/jest.config.js | 11 + x-pack/plugins/fleet/jest.config.js | 11 + x-pack/plugins/global_search/jest.config.js | 11 + .../plugins/global_search_bar/jest.config.js | 11 + .../global_search_providers/jest.config.js | 11 + x-pack/plugins/graph/jest.config.js | 11 + .../index_lifecycle_management/jest.config.js | 11 + .../plugins/index_management/jest.config.js | 11 + x-pack/plugins/infra/jest.config.js | 11 + x-pack/plugins/ingest_manager/jest.config.js | 11 + .../plugins/ingest_pipelines/jest.config.js | 11 + x-pack/plugins/lens/jest.config.js | 11 + .../plugins/license_management/jest.config.js | 11 + x-pack/plugins/licensing/jest.config.js | 11 + x-pack/plugins/lists/jest.config.js | 11 + x-pack/plugins/logstash/jest.config.js | 11 + x-pack/plugins/maps/jest.config.js | 11 + x-pack/plugins/ml/jest.config.js | 11 + x-pack/plugins/monitoring/jest.config.js | 11 + x-pack/plugins/observability/jest.config.js | 35 +- x-pack/plugins/painless_lab/jest.config.js | 11 + x-pack/plugins/remote_clusters/jest.config.js | 11 + x-pack/plugins/reporting/jest.config.js | 11 + x-pack/plugins/rollup/jest.config.js | 11 + x-pack/plugins/runtime_fields/jest.config.js | 11 + .../saved_objects_tagging/jest.config.js | 11 + x-pack/plugins/searchprofiler/jest.config.js | 11 + x-pack/plugins/security/jest.config.js | 11 + .../plugins/security_solution/jest.config.js | 11 + .../plugins/snapshot_restore/jest.config.js | 11 + x-pack/plugins/spaces/jest.config.js | 11 + x-pack/plugins/stack_alerts/jest.config.js | 11 + x-pack/plugins/task_manager/jest.config.js | 11 + .../telemetry_collection_xpack/jest.config.js | 11 + x-pack/plugins/transform/jest.config.js | 11 + .../triggers_actions_ui/jest.config.js | 11 + .../ui_actions_enhanced/jest.config.js | 11 + .../plugins/upgrade_assistant/jest.config.js | 11 + x-pack/plugins/uptime/jest.config.js | 11 + .../jest.config.js | 11 + x-pack/plugins/watcher/jest.config.js | 11 + x-pack/plugins/xpack_legacy/jest.config.js | 11 + x-pack/scripts/jest.js | 14 +- x-pack/scripts/jest_integration.js | 24 - .../monitor_states_real_data.snap | 371 --- .../services/__snapshots__/throughput.snap | 250 --- .../traces/__snapshots__/top_traces.snap | 774 ------- .../__snapshots__/breakdown.snap | 1016 --------- .../__snapshots__/error_rate.snap | 250 --- .../__snapshots__/top_transaction_groups.snap | 126 -- .../__snapshots__/transaction_charts.snap | 1501 ------------- .../csm/__snapshots__/page_load_dist.snap | 824 ------- .../tests/csm/__snapshots__/page_views.snap | 280 --- .../__snapshots__/service_maps.snap | 1995 ----------------- .../transaction_groups_charts.snap | 43 - 177 files changed, 2923 insertions(+), 7690 deletions(-) rename src/dev/jest/config.integration.js => jest.config.integration.js (82%) create mode 100644 jest.config.js create mode 100644 jest.config.oss.js create mode 100644 packages/kbn-apm-config-loader/jest.config.js create mode 100644 packages/kbn-babel-code-parser/jest.config.js create mode 100644 packages/kbn-config-schema/jest.config.js create mode 100644 packages/kbn-config/jest.config.js create mode 100644 packages/kbn-dev-utils/jest.config.js create mode 100644 packages/kbn-es-archiver/jest.config.js create mode 100644 packages/kbn-es/jest.config.js create mode 100644 packages/kbn-i18n/jest.config.js create mode 100644 packages/kbn-interpreter/jest.config.js create mode 100644 packages/kbn-legacy-logging/jest.config.js create mode 100644 packages/kbn-logging/jest.config.js create mode 100644 packages/kbn-monaco/jest.config.js create mode 100644 packages/kbn-optimizer/jest.config.js create mode 100644 packages/kbn-plugin-generator/jest.config.js create mode 100644 packages/kbn-pm/jest.config.js create mode 100644 packages/kbn-release-notes/jest.config.js create mode 100644 packages/kbn-spec-to-console/jest.config.js create mode 100644 packages/kbn-std/jest.config.js create mode 100644 packages/kbn-telemetry-tools/jest.config.js create mode 100644 packages/kbn-test/jest.config.js create mode 100644 packages/kbn-test/src/jest/run_check_jest_configs_cli.ts create mode 100644 packages/kbn-ui-framework/jest.config.js create mode 100644 packages/kbn-utils/jest.config.js rename src/dev/jest/cli.js => scripts/check_jest_configs.js (90%) create mode 100644 src/cli/jest.config.js create mode 100644 src/cli_encryption_keys/jest.config.js create mode 100644 src/cli_keystore/jest.config.js create mode 100644 src/cli_plugin/jest.config.js create mode 100644 src/core/jest.config.js create mode 100644 src/dev/jest.config.js create mode 100644 src/legacy/server/jest.config.js create mode 100644 src/legacy/ui/jest.config.js create mode 100644 src/legacy/utils/jest.config.js create mode 100644 src/optimize/jest.config.js create mode 100644 src/plugins/advanced_settings/jest.config.js create mode 100644 src/plugins/bfetch/jest.config.js create mode 100644 src/plugins/charts/jest.config.js rename src/{dev/jest/config.js => plugins/console/jest.config.js} (59%) create mode 100644 src/plugins/dashboard/jest.config.js create mode 100644 src/plugins/data/jest.config.js create mode 100644 src/plugins/discover/jest.config.js create mode 100644 src/plugins/embeddable/jest.config.js create mode 100644 src/plugins/es_ui_shared/jest.config.js create mode 100644 src/plugins/expressions/jest.config.js create mode 100644 src/plugins/home/jest.config.js create mode 100644 src/plugins/index_pattern_management/jest.config.js create mode 100644 src/plugins/input_control_vis/jest.config.js create mode 100644 src/plugins/inspector/jest.config.js create mode 100644 src/plugins/kibana_legacy/jest.config.js create mode 100644 src/plugins/kibana_overview/jest.config.js create mode 100644 src/plugins/kibana_react/jest.config.js create mode 100644 src/plugins/kibana_usage_collection/jest.config.js create mode 100644 src/plugins/kibana_utils/jest.config.js create mode 100644 src/plugins/legacy_export/jest.config.js create mode 100644 src/plugins/management/jest.config.js create mode 100644 src/plugins/maps_legacy/jest.config.js create mode 100644 src/plugins/navigation/jest.config.js create mode 100644 src/plugins/newsfeed/jest.config.js create mode 100644 src/plugins/region_map/jest.config.js create mode 100644 src/plugins/saved_objects/jest.config.js create mode 100644 src/plugins/saved_objects_management/jest.config.js create mode 100644 src/plugins/saved_objects_tagging_oss/jest.config.js create mode 100644 src/plugins/security_oss/jest.config.js create mode 100644 src/plugins/share/jest.config.js create mode 100644 src/plugins/telemetry/jest.config.js create mode 100644 src/plugins/telemetry_collection_manager/jest.config.js create mode 100644 src/plugins/telemetry_management_section/jest.config.js create mode 100644 src/plugins/tile_map/jest.config.js create mode 100644 src/plugins/ui_actions/jest.config.js create mode 100644 src/plugins/url_forwarding/jest.config.js create mode 100644 src/plugins/usage_collection/jest.config.js create mode 100644 src/plugins/vis_default_editor/jest.config.js create mode 100644 src/plugins/vis_type_markdown/jest.config.js create mode 100644 src/plugins/vis_type_metric/jest.config.js create mode 100644 src/plugins/vis_type_table/jest.config.js create mode 100644 src/plugins/vis_type_tagcloud/jest.config.js create mode 100644 src/plugins/vis_type_timelion/jest.config.js create mode 100644 src/plugins/vis_type_timeseries/jest.config.js create mode 100644 src/plugins/vis_type_vega/jest.config.js create mode 100644 src/plugins/vis_type_vislib/jest.config.js create mode 100644 src/plugins/visualizations/jest.config.js create mode 100644 src/plugins/visualize/jest.config.js create mode 100644 src/setup_node_env/jest.config.js create mode 100644 src/test_utils/jest.config.js create mode 100644 test/functional/jest.config.js create mode 100644 test/scripts/checks/jest_configs.sh delete mode 100644 x-pack/dev-tools/jest/create_jest_config.js delete mode 100644 x-pack/dev-tools/jest/index.js create mode 100644 x-pack/jest.config.js create mode 100644 x-pack/plugins/actions/jest.config.js create mode 100644 x-pack/plugins/alerting_builtins/jest.config.js create mode 100644 x-pack/plugins/alerts/jest.config.js create mode 100644 x-pack/plugins/audit_trail/jest.config.js create mode 100644 x-pack/plugins/beats_management/jest.config.js create mode 100644 x-pack/plugins/canvas/jest.config.js create mode 100644 x-pack/plugins/case/jest.config.js create mode 100644 x-pack/plugins/cloud/jest.config.js create mode 100644 x-pack/plugins/code/jest.config.js create mode 100644 x-pack/plugins/cross_cluster_replication/jest.config.js create mode 100644 x-pack/plugins/dashboard_enhanced/jest.config.js create mode 100644 x-pack/plugins/dashboard_mode/jest.config.js create mode 100644 x-pack/plugins/data_enhanced/jest.config.js create mode 100644 x-pack/plugins/discover_enhanced/jest.config.js create mode 100644 x-pack/plugins/drilldowns/jest.config.js create mode 100644 x-pack/plugins/embeddable_enhanced/jest.config.js create mode 100644 x-pack/plugins/encrypted_saved_objects/jest.config.js create mode 100644 x-pack/plugins/enterprise_search/jest.config.js create mode 100644 x-pack/plugins/event_log/jest.config.js create mode 100644 x-pack/plugins/features/jest.config.js create mode 100644 x-pack/plugins/file_upload/jest.config.js create mode 100644 x-pack/plugins/fleet/jest.config.js create mode 100644 x-pack/plugins/global_search/jest.config.js create mode 100644 x-pack/plugins/global_search_bar/jest.config.js create mode 100644 x-pack/plugins/global_search_providers/jest.config.js create mode 100644 x-pack/plugins/graph/jest.config.js create mode 100644 x-pack/plugins/index_lifecycle_management/jest.config.js create mode 100644 x-pack/plugins/index_management/jest.config.js create mode 100644 x-pack/plugins/infra/jest.config.js create mode 100644 x-pack/plugins/ingest_manager/jest.config.js create mode 100644 x-pack/plugins/ingest_pipelines/jest.config.js create mode 100644 x-pack/plugins/lens/jest.config.js create mode 100644 x-pack/plugins/license_management/jest.config.js create mode 100644 x-pack/plugins/licensing/jest.config.js create mode 100644 x-pack/plugins/lists/jest.config.js create mode 100644 x-pack/plugins/logstash/jest.config.js create mode 100644 x-pack/plugins/maps/jest.config.js create mode 100644 x-pack/plugins/ml/jest.config.js create mode 100644 x-pack/plugins/monitoring/jest.config.js create mode 100644 x-pack/plugins/painless_lab/jest.config.js create mode 100644 x-pack/plugins/remote_clusters/jest.config.js create mode 100644 x-pack/plugins/reporting/jest.config.js create mode 100644 x-pack/plugins/rollup/jest.config.js create mode 100644 x-pack/plugins/runtime_fields/jest.config.js create mode 100644 x-pack/plugins/saved_objects_tagging/jest.config.js create mode 100644 x-pack/plugins/searchprofiler/jest.config.js create mode 100644 x-pack/plugins/security/jest.config.js create mode 100644 x-pack/plugins/security_solution/jest.config.js create mode 100644 x-pack/plugins/snapshot_restore/jest.config.js create mode 100644 x-pack/plugins/spaces/jest.config.js create mode 100644 x-pack/plugins/stack_alerts/jest.config.js create mode 100644 x-pack/plugins/task_manager/jest.config.js create mode 100644 x-pack/plugins/telemetry_collection_xpack/jest.config.js create mode 100644 x-pack/plugins/transform/jest.config.js create mode 100644 x-pack/plugins/triggers_actions_ui/jest.config.js create mode 100644 x-pack/plugins/ui_actions_enhanced/jest.config.js create mode 100644 x-pack/plugins/upgrade_assistant/jest.config.js create mode 100644 x-pack/plugins/uptime/jest.config.js create mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js create mode 100644 x-pack/plugins/watcher/jest.config.js create mode 100644 x-pack/plugins/xpack_legacy/jest.config.js delete mode 100644 x-pack/scripts/jest_integration.js delete mode 100644 x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap delete mode 100644 x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap delete mode 100644 x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap delete mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap delete mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap delete mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap delete mode 100644 x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap delete mode 100644 x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap diff --git a/docs/developer/contributing/development-unit-tests.asciidoc b/docs/developer/contributing/development-unit-tests.asciidoc index 5322106b17ac1..d5f5bc76b3302 100644 --- a/docs/developer/contributing/development-unit-tests.asciidoc +++ b/docs/developer/contributing/development-unit-tests.asciidoc @@ -20,11 +20,13 @@ yarn test:mocha == Jest Jest tests are stored in the same directory as source code files with the `.test.{js,mjs,ts,tsx}` suffix. -*Running Jest Unit Tests* +Each plugin and package contains it's own `jest.config.js` file to define its root, and any overrides +to the jest-preset provided by `@kbn/test`. When working on a single plugin or package, you will find +it's more efficient to supply the Jest configuration file when running. ["source","shell"] ----------- -yarn test:jest +yarn jest --config src/plugins/discover/jest.config.js ----------- [discrete] diff --git a/src/dev/jest/config.integration.js b/jest.config.integration.js similarity index 82% rename from src/dev/jest/config.integration.js rename to jest.config.integration.js index 9e7bbc34ac711..3dacb107f94c0 100644 --- a/src/dev/jest/config.integration.js +++ b/jest.config.integration.js @@ -17,16 +17,14 @@ * under the License. */ -import preset from '@kbn/test/jest-preset'; -import config from './config'; +const preset = require('@kbn/test/jest-preset'); -export default { - ...config, - testMatch: [ - '**/integration_tests/**/*.test.js', - '**/integration_tests/**/*.test.ts', - '**/integration_tests/**/*.test.tsx', - ], +module.exports = { + preset: '@kbn/test', + rootDir: '.', + roots: ['<rootDir>/src', '<rootDir>/packages'], + testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], + testRunner: 'jasmine2', testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000000..c190556700b81 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + rootDir: '.', + projects: [...require('./jest.config.oss').projects, ...require('./x-pack/jest.config').projects], +}; diff --git a/jest.config.oss.js b/jest.config.oss.js new file mode 100644 index 0000000000000..e9235069687e0 --- /dev/null +++ b/jest.config.oss.js @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + rootDir: '.', + projects: [ + '<rootDir>/packages/*/jest.config.js', + '<rootDir>/src/*/jest.config.js', + '<rootDir>/src/legacy/*/jest.config.js', + '<rootDir>/src/plugins/*/jest.config.js', + '<rootDir>/test/*/jest.config.js', + ], + reporters: ['default', '<rootDir>/packages/kbn-test/target/jest/junit_reporter'], +}; diff --git a/packages/README.md b/packages/README.md index 8ff05f4e8ff89..9d9cd4ed7b6e5 100644 --- a/packages/README.md +++ b/packages/README.md @@ -60,7 +60,7 @@ A package can also follow the pattern of having `.test.js` files as siblings of A package using the `.test.js` naming convention will have those tests automatically picked up by Jest and run by the unit test runner, currently mapped to the Kibana `test` script in the root `package.json`. * `yarn test` or `yarn grunt test` runs all unit tests. -* `node scripts/jest` runs all Jest tests in Kibana. +* `yarn jest` runs all Jest tests in Kibana. ---- Each package can also specify its own `test` script in the package's `package.json`, for cases where you'd prefer to run the tests from the local package directory. diff --git a/packages/kbn-apm-config-loader/jest.config.js b/packages/kbn-apm-config-loader/jest.config.js new file mode 100644 index 0000000000000..2b88679a57e72 --- /dev/null +++ b/packages/kbn-apm-config-loader/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-apm-config-loader'], +}; diff --git a/packages/kbn-babel-code-parser/jest.config.js b/packages/kbn-babel-code-parser/jest.config.js new file mode 100644 index 0000000000000..60fce8897723e --- /dev/null +++ b/packages/kbn-babel-code-parser/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-babel-code-parser'], +}; diff --git a/packages/kbn-config-schema/jest.config.js b/packages/kbn-config-schema/jest.config.js new file mode 100644 index 0000000000000..35de02838aa1c --- /dev/null +++ b/packages/kbn-config-schema/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-config-schema'], +}; diff --git a/packages/kbn-config/jest.config.js b/packages/kbn-config/jest.config.js new file mode 100644 index 0000000000000..b4c84eef4675c --- /dev/null +++ b/packages/kbn-config/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-config'], +}; diff --git a/packages/kbn-dev-utils/jest.config.js b/packages/kbn-dev-utils/jest.config.js new file mode 100644 index 0000000000000..2b0cefe5e741f --- /dev/null +++ b/packages/kbn-dev-utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-dev-utils'], +}; diff --git a/packages/kbn-es-archiver/jest.config.js b/packages/kbn-es-archiver/jest.config.js new file mode 100644 index 0000000000000..e5df757f6637e --- /dev/null +++ b/packages/kbn-es-archiver/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-es-archiver'], +}; diff --git a/packages/kbn-es/jest.config.js b/packages/kbn-es/jest.config.js new file mode 100644 index 0000000000000..2c09b5400369d --- /dev/null +++ b/packages/kbn-es/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-es'], +}; diff --git a/packages/kbn-i18n/jest.config.js b/packages/kbn-i18n/jest.config.js new file mode 100644 index 0000000000000..dff8b872bdfe0 --- /dev/null +++ b/packages/kbn-i18n/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-i18n'], + testRunner: 'jasmine2', +}; diff --git a/packages/kbn-interpreter/jest.config.js b/packages/kbn-interpreter/jest.config.js new file mode 100644 index 0000000000000..d2f6127ccff79 --- /dev/null +++ b/packages/kbn-interpreter/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-interpreter'], +}; diff --git a/packages/kbn-legacy-logging/jest.config.js b/packages/kbn-legacy-logging/jest.config.js new file mode 100644 index 0000000000000..f33205439e134 --- /dev/null +++ b/packages/kbn-legacy-logging/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-legacy-logging'], +}; diff --git a/packages/kbn-logging/jest.config.js b/packages/kbn-logging/jest.config.js new file mode 100644 index 0000000000000..74ff8fd14f56a --- /dev/null +++ b/packages/kbn-logging/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-logging'], +}; diff --git a/packages/kbn-monaco/jest.config.js b/packages/kbn-monaco/jest.config.js new file mode 100644 index 0000000000000..03f879f6d0bef --- /dev/null +++ b/packages/kbn-monaco/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-monaco'], +}; diff --git a/packages/kbn-optimizer/jest.config.js b/packages/kbn-optimizer/jest.config.js new file mode 100644 index 0000000000000..6e313aaad3c82 --- /dev/null +++ b/packages/kbn-optimizer/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-optimizer'], +}; diff --git a/packages/kbn-plugin-generator/jest.config.js b/packages/kbn-plugin-generator/jest.config.js new file mode 100644 index 0000000000000..1d81a72128afd --- /dev/null +++ b/packages/kbn-plugin-generator/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-plugin-generator'], +}; diff --git a/packages/kbn-pm/jest.config.js b/packages/kbn-pm/jest.config.js new file mode 100644 index 0000000000000..ba0624f5f6ccd --- /dev/null +++ b/packages/kbn-pm/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-pm'], +}; diff --git a/packages/kbn-release-notes/jest.config.js b/packages/kbn-release-notes/jest.config.js new file mode 100644 index 0000000000000..44390a8c98162 --- /dev/null +++ b/packages/kbn-release-notes/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-release-notes'], +}; diff --git a/packages/kbn-spec-to-console/jest.config.js b/packages/kbn-spec-to-console/jest.config.js new file mode 100644 index 0000000000000..cef82f4d76f73 --- /dev/null +++ b/packages/kbn-spec-to-console/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-spec-to-console'], +}; diff --git a/packages/kbn-std/jest.config.js b/packages/kbn-std/jest.config.js new file mode 100644 index 0000000000000..0615e33e41af8 --- /dev/null +++ b/packages/kbn-std/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-std'], +}; diff --git a/packages/kbn-telemetry-tools/jest.config.js b/packages/kbn-telemetry-tools/jest.config.js new file mode 100644 index 0000000000000..b7b101beccf32 --- /dev/null +++ b/packages/kbn-telemetry-tools/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-telemetry-tools'], +}; diff --git a/packages/kbn-test/jest.config.js b/packages/kbn-test/jest.config.js new file mode 100644 index 0000000000000..9400d402a1a33 --- /dev/null +++ b/packages/kbn-test/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-test'], +}; diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 9e6ba67a421ac..54b064f5cd49e 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -60,3 +60,5 @@ export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix'; export * from './functional_test_runner'; export { getUrl } from './jest/utils/get_url'; + +export { runCheckJestConfigsCli } from './jest/run_check_jest_configs_cli'; diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts new file mode 100644 index 0000000000000..385fb453697ef --- /dev/null +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { relative, resolve, sep } from 'path'; +import { writeFileSync } from 'fs'; + +import execa from 'execa'; +import globby from 'globby'; +import Mustache from 'mustache'; + +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; + +// @ts-ignore +import { testMatch } from '../../jest-preset'; + +const template: string = `module.exports = { + preset: '@kbn/test', + rootDir: '{{{relToRoot}}}', + roots: ['<rootDir>/{{{modulePath}}}'], +}; +`; + +const roots: string[] = ['x-pack/plugins', 'packages', 'src/legacy', 'src/plugins', 'test', 'src']; + +export async function runCheckJestConfigsCli() { + run( + async ({ flags: { fix = false }, log }) => { + const { stdout: coveredFiles } = await execa( + 'yarn', + ['--silent', 'jest', '--listTests', '--json'], + { + cwd: REPO_ROOT, + } + ); + + const allFiles = new Set( + await globby(testMatch.concat(['!**/integration_tests/**']), { + gitignore: true, + }) + ); + + JSON.parse(coveredFiles).forEach((file: string) => { + const pathFromRoot = relative(REPO_ROOT, file); + allFiles.delete(pathFromRoot); + }); + + if (allFiles.size) { + log.error( + `The following files do not belong to a jest.config.js file, or that config is not included from the root jest.config.js\n${[ + ...allFiles, + ] + .map((file) => ` - ${file}`) + .join('\n')}` + ); + } else { + log.success('All test files are included by a Jest configuration'); + return; + } + + if (fix) { + allFiles.forEach((file) => { + const root = roots.find((r) => file.startsWith(r)); + + if (root) { + const name = relative(root, file).split(sep)[0]; + const modulePath = [root, name].join('/'); + + const content = Mustache.render(template, { + relToRoot: relative(modulePath, '.'), + modulePath, + }); + + writeFileSync(resolve(root, name, 'jest.config.js'), content); + } else { + log.warning(`Unable to determind where to place jest.config.js for ${file}`); + } + }); + } else { + log.info( + `Run 'node scripts/check_jest_configs --fix' to attempt to create the missing config files` + ); + } + + process.exit(1); + }, + { + description: 'Check that all test files are covered by a jest.config.js', + flags: { + boolean: ['fix'], + help: ` + --fix Attempt to create missing config files + `, + }, + } + ); +} diff --git a/packages/kbn-ui-framework/jest.config.js b/packages/kbn-ui-framework/jest.config.js new file mode 100644 index 0000000000000..d9cb93d7c069d --- /dev/null +++ b/packages/kbn-ui-framework/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-ui-framework'], +}; diff --git a/packages/kbn-utils/jest.config.js b/packages/kbn-utils/jest.config.js new file mode 100644 index 0000000000000..39fb0a8ff1a8c --- /dev/null +++ b/packages/kbn-utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-utils'], +}; diff --git a/src/dev/jest/cli.js b/scripts/check_jest_configs.js similarity index 90% rename from src/dev/jest/cli.js rename to scripts/check_jest_configs.js index 40627c4bece74..a7a520f433bf9 100644 --- a/src/dev/jest/cli.js +++ b/scripts/check_jest_configs.js @@ -17,6 +17,5 @@ * under the License. */ -import { run } from 'jest'; - -run(process.argv.slice(2)); +require('../src/setup_node_env'); +require('@kbn/test').runCheckJestConfigsCli(); diff --git a/scripts/jest.js b/scripts/jest.js index c252056de766b..90f8da10f4c90 100755 --- a/scripts/jest.js +++ b/scripts/jest.js @@ -29,8 +29,15 @@ // // See all cli options in https://facebook.github.io/jest/docs/cli.html -var resolve = require('path').resolve; -process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.js')); +if (process.argv.indexOf('--config') === -1) { + // append correct jest.config if none is provided + var configPath = require('path').resolve(__dirname, '../jest.config.oss.js'); + process.argv.push('--config', configPath); + console.log('Running Jest with --config', configPath); +} -require('../src/setup_node_env'); -require('../src/dev/jest/cli'); +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/scripts/jest_integration.js b/scripts/jest_integration.js index 7da1436f5583c..f07d28939ef0c 100755 --- a/scripts/jest_integration.js +++ b/scripts/jest_integration.js @@ -29,9 +29,17 @@ // // See all cli options in https://facebook.github.io/jest/docs/cli.html -var resolve = require('path').resolve; -process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.integration.js')); process.argv.push('--runInBand'); -require('../src/setup_node_env'); -require('../src/dev/jest/cli'); +if (process.argv.indexOf('--config') === -1) { + // append correct jest.config if none is provided + var configPath = require('path').resolve(__dirname, '../jest.config.integration.js'); + process.argv.push('--config', configPath); + console.log('Running Jest with --config', configPath); +} + +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/src/cli/jest.config.js b/src/cli/jest.config.js new file mode 100644 index 0000000000000..6a1055ca864c8 --- /dev/null +++ b/src/cli/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/cli'], +}; diff --git a/src/cli_encryption_keys/jest.config.js b/src/cli_encryption_keys/jest.config.js new file mode 100644 index 0000000000000..f3be28f7898f5 --- /dev/null +++ b/src/cli_encryption_keys/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/cli_encryption_keys'], +}; diff --git a/src/cli_keystore/jest.config.js b/src/cli_keystore/jest.config.js new file mode 100644 index 0000000000000..787cd7ccd84be --- /dev/null +++ b/src/cli_keystore/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/cli_keystore'], +}; diff --git a/src/cli_plugin/jest.config.js b/src/cli_plugin/jest.config.js new file mode 100644 index 0000000000000..cbd226f5df887 --- /dev/null +++ b/src/cli_plugin/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/cli_plugin'], +}; diff --git a/src/core/jest.config.js b/src/core/jest.config.js new file mode 100644 index 0000000000000..bdb65b3817507 --- /dev/null +++ b/src/core/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/core'], + testRunner: 'jasmine2', +}; diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js index c666581ddb08c..177439c56a115 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js @@ -49,7 +49,8 @@ describe('Team Assignment', () => { describe(`when the codeowners file contains #CC#`, () => { it(`should strip the prefix and still drill down through the fs`, async () => { const { stdout } = await execa('grep', ['tre', teamAssignmentsPath], { cwd: ROOT_DIR }); - expect(stdout).to.be(`x-pack/plugins/code/server/config.ts kibana-tre + expect(stdout).to.be(`x-pack/plugins/code/jest.config.js kibana-tre +x-pack/plugins/code/server/config.ts kibana-tre x-pack/plugins/code/server/index.ts kibana-tre x-pack/plugins/code/server/plugin.test.ts kibana-tre x-pack/plugins/code/server/plugin.ts kibana-tre`); diff --git a/src/dev/jest.config.js b/src/dev/jest.config.js new file mode 100644 index 0000000000000..bdb51372e2c26 --- /dev/null +++ b/src/dev/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/dev'], +}; diff --git a/src/legacy/server/jest.config.js b/src/legacy/server/jest.config.js new file mode 100644 index 0000000000000..f971e823765ac --- /dev/null +++ b/src/legacy/server/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/legacy/server'], +}; diff --git a/src/legacy/ui/jest.config.js b/src/legacy/ui/jest.config.js new file mode 100644 index 0000000000000..45809f8797129 --- /dev/null +++ b/src/legacy/ui/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/legacy/ui'], +}; diff --git a/src/legacy/utils/jest.config.js b/src/legacy/utils/jest.config.js new file mode 100644 index 0000000000000..7ce73fa367613 --- /dev/null +++ b/src/legacy/utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/legacy/utils'], +}; diff --git a/src/optimize/jest.config.js b/src/optimize/jest.config.js new file mode 100644 index 0000000000000..419f4f97098b3 --- /dev/null +++ b/src/optimize/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/optimize'], +}; diff --git a/src/plugins/advanced_settings/jest.config.js b/src/plugins/advanced_settings/jest.config.js new file mode 100644 index 0000000000000..94fd65aae4464 --- /dev/null +++ b/src/plugins/advanced_settings/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/advanced_settings'], +}; diff --git a/src/plugins/bfetch/jest.config.js b/src/plugins/bfetch/jest.config.js new file mode 100644 index 0000000000000..5976a994be7e5 --- /dev/null +++ b/src/plugins/bfetch/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/bfetch'], +}; diff --git a/src/plugins/charts/jest.config.js b/src/plugins/charts/jest.config.js new file mode 100644 index 0000000000000..168ccde71a667 --- /dev/null +++ b/src/plugins/charts/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/charts'], +}; diff --git a/src/dev/jest/config.js b/src/plugins/console/jest.config.js similarity index 59% rename from src/dev/jest/config.js rename to src/plugins/console/jest.config.js index c04ef9480d0b7..f08613f91e1f1 100644 --- a/src/dev/jest/config.js +++ b/src/plugins/console/jest.config.js @@ -17,26 +17,9 @@ * under the License. */ -export default { +module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: [ - '<rootDir>/src/plugins', - '<rootDir>/src/legacy/ui', - '<rootDir>/src/core', - '<rootDir>/src/legacy/server', - '<rootDir>/src/cli', - '<rootDir>/src/cli_keystore', - '<rootDir>/src/cli_encryption_keys', - '<rootDir>/src/cli_plugin', - '<rootDir>/packages/kbn-test/target/functional_test_runner', - '<rootDir>/src/dev', - '<rootDir>/src/optimize', - '<rootDir>/src/legacy/utils', - '<rootDir>/src/setup_node_env', - '<rootDir>/packages', - '<rootDir>/test/functional/services/remote', - '<rootDir>/src/dev/code_coverage/ingest_coverage', - ], + roots: ['<rootDir>/src/plugins/console'], testRunner: 'jasmine2', }; diff --git a/src/plugins/dashboard/jest.config.js b/src/plugins/dashboard/jest.config.js new file mode 100644 index 0000000000000..b9f6f66159b30 --- /dev/null +++ b/src/plugins/dashboard/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/dashboard'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/data/jest.config.js b/src/plugins/data/jest.config.js new file mode 100644 index 0000000000000..3c6e854a53d7b --- /dev/null +++ b/src/plugins/data/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/data'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/discover/jest.config.js b/src/plugins/discover/jest.config.js new file mode 100644 index 0000000000000..0723569db357d --- /dev/null +++ b/src/plugins/discover/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/discover'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/embeddable/jest.config.js b/src/plugins/embeddable/jest.config.js new file mode 100644 index 0000000000000..a079791092549 --- /dev/null +++ b/src/plugins/embeddable/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/embeddable'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/es_ui_shared/jest.config.js b/src/plugins/es_ui_shared/jest.config.js new file mode 100644 index 0000000000000..5b8b34692800d --- /dev/null +++ b/src/plugins/es_ui_shared/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/es_ui_shared'], +}; diff --git a/src/plugins/expressions/jest.config.js b/src/plugins/expressions/jest.config.js new file mode 100644 index 0000000000000..b4e3e10b3fc70 --- /dev/null +++ b/src/plugins/expressions/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/expressions'], +}; diff --git a/src/plugins/home/jest.config.js b/src/plugins/home/jest.config.js new file mode 100644 index 0000000000000..c56c7b3eed1d6 --- /dev/null +++ b/src/plugins/home/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/home'], +}; diff --git a/src/plugins/index_pattern_management/jest.config.js b/src/plugins/index_pattern_management/jest.config.js new file mode 100644 index 0000000000000..8a499406080fd --- /dev/null +++ b/src/plugins/index_pattern_management/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/index_pattern_management'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/input_control_vis/jest.config.js b/src/plugins/input_control_vis/jest.config.js new file mode 100644 index 0000000000000..17fb6f3359bf3 --- /dev/null +++ b/src/plugins/input_control_vis/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/input_control_vis'], +}; diff --git a/src/plugins/inspector/jest.config.js b/src/plugins/inspector/jest.config.js new file mode 100644 index 0000000000000..6fc4a063970b9 --- /dev/null +++ b/src/plugins/inspector/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/inspector'], +}; diff --git a/src/plugins/kibana_legacy/jest.config.js b/src/plugins/kibana_legacy/jest.config.js new file mode 100644 index 0000000000000..69df43bc5b15f --- /dev/null +++ b/src/plugins/kibana_legacy/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/kibana_legacy'], +}; diff --git a/src/plugins/kibana_overview/jest.config.js b/src/plugins/kibana_overview/jest.config.js new file mode 100644 index 0000000000000..4a719b38e47ae --- /dev/null +++ b/src/plugins/kibana_overview/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/kibana_overview'], +}; diff --git a/src/plugins/kibana_react/jest.config.js b/src/plugins/kibana_react/jest.config.js new file mode 100644 index 0000000000000..2810331c9b667 --- /dev/null +++ b/src/plugins/kibana_react/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/kibana_react'], +}; diff --git a/src/plugins/kibana_usage_collection/jest.config.js b/src/plugins/kibana_usage_collection/jest.config.js new file mode 100644 index 0000000000000..9510fc98732b3 --- /dev/null +++ b/src/plugins/kibana_usage_collection/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/kibana_usage_collection'], +}; diff --git a/src/plugins/kibana_utils/jest.config.js b/src/plugins/kibana_utils/jest.config.js new file mode 100644 index 0000000000000..2ddfb7047bf2e --- /dev/null +++ b/src/plugins/kibana_utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/kibana_utils'], +}; diff --git a/src/plugins/legacy_export/jest.config.js b/src/plugins/legacy_export/jest.config.js new file mode 100644 index 0000000000000..1480049fd8f85 --- /dev/null +++ b/src/plugins/legacy_export/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/legacy_export'], +}; diff --git a/src/plugins/management/jest.config.js b/src/plugins/management/jest.config.js new file mode 100644 index 0000000000000..287bafc4b1c11 --- /dev/null +++ b/src/plugins/management/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/management'], +}; diff --git a/src/plugins/maps_legacy/jest.config.js b/src/plugins/maps_legacy/jest.config.js new file mode 100644 index 0000000000000..849bd2957ba62 --- /dev/null +++ b/src/plugins/maps_legacy/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/maps_legacy'], +}; diff --git a/src/plugins/navigation/jest.config.js b/src/plugins/navigation/jest.config.js new file mode 100644 index 0000000000000..bc999a25854de --- /dev/null +++ b/src/plugins/navigation/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/navigation'], +}; diff --git a/src/plugins/newsfeed/jest.config.js b/src/plugins/newsfeed/jest.config.js new file mode 100644 index 0000000000000..bf530497bcbad --- /dev/null +++ b/src/plugins/newsfeed/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/newsfeed'], +}; diff --git a/src/plugins/region_map/jest.config.js b/src/plugins/region_map/jest.config.js new file mode 100644 index 0000000000000..c0d4e4d40bb3a --- /dev/null +++ b/src/plugins/region_map/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/region_map'], +}; diff --git a/src/plugins/saved_objects/jest.config.js b/src/plugins/saved_objects/jest.config.js new file mode 100644 index 0000000000000..00ab010bc61ba --- /dev/null +++ b/src/plugins/saved_objects/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/saved_objects'], +}; diff --git a/src/plugins/saved_objects_management/jest.config.js b/src/plugins/saved_objects_management/jest.config.js new file mode 100644 index 0000000000000..3cedb8c937f5e --- /dev/null +++ b/src/plugins/saved_objects_management/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/saved_objects_management'], +}; diff --git a/src/plugins/saved_objects_tagging_oss/jest.config.js b/src/plugins/saved_objects_tagging_oss/jest.config.js new file mode 100644 index 0000000000000..7e75b5c5593e7 --- /dev/null +++ b/src/plugins/saved_objects_tagging_oss/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/saved_objects_tagging_oss'], +}; diff --git a/src/plugins/security_oss/jest.config.js b/src/plugins/security_oss/jest.config.js new file mode 100644 index 0000000000000..3bf6ee33d3e48 --- /dev/null +++ b/src/plugins/security_oss/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/security_oss'], +}; diff --git a/src/plugins/share/jest.config.js b/src/plugins/share/jest.config.js new file mode 100644 index 0000000000000..39b048279e73b --- /dev/null +++ b/src/plugins/share/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/share'], +}; diff --git a/src/plugins/telemetry/jest.config.js b/src/plugins/telemetry/jest.config.js new file mode 100644 index 0000000000000..914cea68cd01b --- /dev/null +++ b/src/plugins/telemetry/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/telemetry'], +}; diff --git a/src/plugins/telemetry_collection_manager/jest.config.js b/src/plugins/telemetry_collection_manager/jest.config.js new file mode 100644 index 0000000000000..9278ca21d7bc2 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/telemetry_collection_manager'], +}; diff --git a/src/plugins/telemetry_management_section/jest.config.js b/src/plugins/telemetry_management_section/jest.config.js new file mode 100644 index 0000000000000..a38fa84b08afc --- /dev/null +++ b/src/plugins/telemetry_management_section/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/telemetry_management_section'], +}; diff --git a/src/plugins/tile_map/jest.config.js b/src/plugins/tile_map/jest.config.js new file mode 100644 index 0000000000000..9a89247b4f782 --- /dev/null +++ b/src/plugins/tile_map/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/tile_map'], +}; diff --git a/src/plugins/ui_actions/jest.config.js b/src/plugins/ui_actions/jest.config.js new file mode 100644 index 0000000000000..3a7de575ea248 --- /dev/null +++ b/src/plugins/ui_actions/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/ui_actions'], +}; diff --git a/src/plugins/url_forwarding/jest.config.js b/src/plugins/url_forwarding/jest.config.js new file mode 100644 index 0000000000000..9dcbfccfcf90a --- /dev/null +++ b/src/plugins/url_forwarding/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/url_forwarding'], +}; diff --git a/src/plugins/usage_collection/jest.config.js b/src/plugins/usage_collection/jest.config.js new file mode 100644 index 0000000000000..89b7fc70fd620 --- /dev/null +++ b/src/plugins/usage_collection/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/usage_collection'], +}; diff --git a/src/plugins/vis_default_editor/jest.config.js b/src/plugins/vis_default_editor/jest.config.js new file mode 100644 index 0000000000000..618f9734fb54c --- /dev/null +++ b/src/plugins/vis_default_editor/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_default_editor'], +}; diff --git a/src/plugins/vis_type_markdown/jest.config.js b/src/plugins/vis_type_markdown/jest.config.js new file mode 100644 index 0000000000000..bff1b12641c92 --- /dev/null +++ b/src/plugins/vis_type_markdown/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_markdown'], +}; diff --git a/src/plugins/vis_type_metric/jest.config.js b/src/plugins/vis_type_metric/jest.config.js new file mode 100644 index 0000000000000..5c50fc5f4368e --- /dev/null +++ b/src/plugins/vis_type_metric/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_metric'], +}; diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js new file mode 100644 index 0000000000000..3aa02089df012 --- /dev/null +++ b/src/plugins/vis_type_table/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_table'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/vis_type_tagcloud/jest.config.js b/src/plugins/vis_type_tagcloud/jest.config.js new file mode 100644 index 0000000000000..5419ca05cca84 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/jest.config.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_tagcloud'], + testRunner: 'jasmine2', +}; diff --git a/src/plugins/vis_type_timelion/jest.config.js b/src/plugins/vis_type_timelion/jest.config.js new file mode 100644 index 0000000000000..eae12936427f4 --- /dev/null +++ b/src/plugins/vis_type_timelion/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_timelion'], +}; diff --git a/src/plugins/vis_type_timeseries/jest.config.js b/src/plugins/vis_type_timeseries/jest.config.js new file mode 100644 index 0000000000000..16c001e598188 --- /dev/null +++ b/src/plugins/vis_type_timeseries/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_timeseries'], +}; diff --git a/src/plugins/vis_type_vega/jest.config.js b/src/plugins/vis_type_vega/jest.config.js new file mode 100644 index 0000000000000..a9ae68df0d89b --- /dev/null +++ b/src/plugins/vis_type_vega/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_vega'], +}; diff --git a/src/plugins/vis_type_vislib/jest.config.js b/src/plugins/vis_type_vislib/jest.config.js new file mode 100644 index 0000000000000..1324ec1404b3e --- /dev/null +++ b/src/plugins/vis_type_vislib/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/vis_type_vislib'], +}; diff --git a/src/plugins/visualizations/jest.config.js b/src/plugins/visualizations/jest.config.js new file mode 100644 index 0000000000000..b1c5067cfe4a9 --- /dev/null +++ b/src/plugins/visualizations/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/visualizations'], +}; diff --git a/src/plugins/visualize/jest.config.js b/src/plugins/visualize/jest.config.js new file mode 100644 index 0000000000000..6657f4092068f --- /dev/null +++ b/src/plugins/visualize/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/src/plugins/visualize'], +}; diff --git a/src/setup_node_env/jest.config.js b/src/setup_node_env/jest.config.js new file mode 100644 index 0000000000000..61e3239905836 --- /dev/null +++ b/src/setup_node_env/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/setup_node_env'], +}; diff --git a/src/test_utils/jest.config.js b/src/test_utils/jest.config.js new file mode 100644 index 0000000000000..b7e77413598c0 --- /dev/null +++ b/src/test_utils/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/src/test_utils'], +}; diff --git a/test/functional/jest.config.js b/test/functional/jest.config.js new file mode 100644 index 0000000000000..60dce5d95773a --- /dev/null +++ b/test/functional/jest.config.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/test/functional'], +}; diff --git a/test/scripts/checks/jest_configs.sh b/test/scripts/checks/jest_configs.sh new file mode 100644 index 0000000000000..28cb1386c748f --- /dev/null +++ b/test/scripts/checks/jest_configs.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +checks-reporter-with-killswitch "Check Jest Configs" node scripts/check_jest_configs diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 89302e91ad479..fd96c2bbf8e78 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -6,6 +6,7 @@ def check() { tasks([ kibanaPipeline.scriptTask('Check Telemetry Schema', 'test/scripts/checks/telemetry.sh'), kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'), + kibanaPipeline.scriptTask('Check Jest Configs', 'test/scripts/checks/jest_configs.sh'), kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'), kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'), kibanaPipeline.scriptTask('Check Bundle Limits', 'test/scripts/checks/bundle_limits.sh'), diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js deleted file mode 100644 index 2ddc58500d15e..0000000000000 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function createJestConfig({ kibanaDirectory, rootDir }) { - return { - preset: '@kbn/test', - rootDir: kibanaDirectory, - roots: [`${rootDir}/plugins`], - reporters: [ - 'default', - [ - `${kibanaDirectory}/packages/kbn-test/target/jest/junit_reporter`, - { - reportName: 'X-Pack Jest Tests', - }, - ], - ], - }; -} diff --git a/x-pack/dev-tools/jest/index.js b/x-pack/dev-tools/jest/index.js deleted file mode 100644 index c22f8625c5778..0000000000000 --- a/x-pack/dev-tools/jest/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { run } from 'jest'; -import { resolve } from 'path'; - -import { createJestConfig } from './create_jest_config'; - -export function runJest() { - process.env.NODE_ENV = process.env.NODE_ENV || 'test'; - const config = JSON.stringify( - createJestConfig({ - kibanaDirectory: resolve(__dirname, '../../..'), - rootDir: resolve(__dirname, '../..'), - xPackKibanaDirectory: resolve(__dirname, '../..'), - }) - ); - - const argv = [...process.argv.slice(2), '--config', config]; - - return run(argv); -} diff --git a/x-pack/jest.config.js b/x-pack/jest.config.js new file mode 100644 index 0000000000000..8b3f717b40e66 --- /dev/null +++ b/x-pack/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + rootDir: '..', + projects: ['<rootDir>/x-pack/plugins/*/jest.config.js'], + reporters: [ + 'default', + ['<rootDir>/packages/kbn-test/target/jest/junit_reporter', { reportName: 'X-Pack Jest Tests' }], + ], +}; diff --git a/x-pack/plugins/actions/jest.config.js b/x-pack/plugins/actions/jest.config.js new file mode 100644 index 0000000000000..2aaa277079ad3 --- /dev/null +++ b/x-pack/plugins/actions/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/actions'], +}; diff --git a/x-pack/plugins/alerting_builtins/jest.config.js b/x-pack/plugins/alerting_builtins/jest.config.js new file mode 100644 index 0000000000000..05fe793a157df --- /dev/null +++ b/x-pack/plugins/alerting_builtins/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/alerting_builtins'], +}; diff --git a/x-pack/plugins/alerts/jest.config.js b/x-pack/plugins/alerts/jest.config.js new file mode 100644 index 0000000000000..d5c9ce5bbf4c2 --- /dev/null +++ b/x-pack/plugins/alerts/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/alerts'], +}; diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 2a5ef9ad0c2a7..a0e98eebf65cb 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -4,34 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// This is an APM-specific Jest configuration which overrides the x-pack -// configuration. It's intended for use in development and does not run in CI, -// which runs the entire x-pack suite. Run `npx jest`. - -require('../../../src/setup_node_env'); - -const { createJestConfig } = require('../../dev-tools/jest/create_jest_config'); -const { resolve } = require('path'); - -const rootDir = resolve(__dirname, '.'); -const kibanaDirectory = resolve(__dirname, '../../..'); - -const jestConfig = createJestConfig({ kibanaDirectory, rootDir }); - module.exports = { - ...jestConfig, - reporters: ['default'], - roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], - collectCoverage: true, - collectCoverageFrom: [ - ...(jestConfig.collectCoverageFrom || []), - '**/*.{js,mjs,jsx,ts,tsx}', - '!**/*.stories.{js,mjs,ts,tsx}', - '!**/dev_docs/**', - '!**/e2e/**', - '!**/target/**', - '!**/typings/**', - ], - coverageDirectory: `${rootDir}/target/coverage/jest`, - coverageReporters: ['html'], + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/apm'], }; diff --git a/x-pack/plugins/audit_trail/jest.config.js b/x-pack/plugins/audit_trail/jest.config.js new file mode 100644 index 0000000000000..31de78fc6bbd9 --- /dev/null +++ b/x-pack/plugins/audit_trail/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/audit_trail'], +}; diff --git a/x-pack/plugins/beats_management/jest.config.js b/x-pack/plugins/beats_management/jest.config.js new file mode 100644 index 0000000000000..8ffbb97b1656d --- /dev/null +++ b/x-pack/plugins/beats_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/beats_management'], +}; diff --git a/x-pack/plugins/canvas/jest.config.js b/x-pack/plugins/canvas/jest.config.js new file mode 100644 index 0000000000000..d010fb8c150bc --- /dev/null +++ b/x-pack/plugins/canvas/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/canvas'], +}; diff --git a/x-pack/plugins/canvas/scripts/jest.js b/x-pack/plugins/canvas/scripts/jest.js index a91431a0141c5..9dd8bac88e1e2 100644 --- a/x-pack/plugins/canvas/scripts/jest.js +++ b/x-pack/plugins/canvas/scripts/jest.js @@ -4,89 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -const { run } = require('@kbn/dev-utils'); -const { runXPackScript } = require('./_helpers'); +const { resolve } = require('path'); +process.argv.push('--config', resolve(__dirname, '../jest.config.js')); -// Due to https://github.com/facebook/jest/issues/7267, folders that start with `.` -// are ignored if using watchman. Disabling watchman makes testing slow. So -// we're making this script allow -run( - ({ log, flags }) => { - const { all, storybook, update, coverage } = flags; - let { path } = flags; - let options = []; - process.argv.splice(2, process.argv.length - 2); +const storybookPosition = process.argv.indexOf('--storybook'); +const allPosition = process.argv.indexOf('--all'); - if (path) { - log.info(`Limiting tests to ${path}...`); - path = 'plugins/canvas/' + path; - } else { - path = 'plugins/canvas'; - } +console.log(` +A helper proxying to the following command: - if (coverage) { - log.info(`Collecting test coverage and writing to canvas/coverage...`); - options = [ - '--coverage', - '--collectCoverageFrom', // Ignore TS definition files - `!${path}/**/*.d.ts`, - '--collectCoverageFrom', // Ignore build directories - `!${path}/**/build/**`, - '--collectCoverageFrom', // Ignore coverage on test files - `!${path}/**/__tests__/**/*`, - '--collectCoverageFrom', // Ignore coverage on example files - `!${path}/**/__examples__/**/*`, - '--collectCoverageFrom', // Ignore flot files - `!${path}/**/flot-charts/**`, - '--collectCoverageFrom', // Ignore coverage files - `!${path}/**/coverage/**`, - '--collectCoverageFrom', // Ignore scripts - `!${path}/**/scripts/**`, - '--collectCoverageFrom', // Ignore mock files - `!${path}/**/mocks/**`, - '--collectCoverageFrom', // Include JS files - `${path}/**/*.js`, - '--collectCoverageFrom', // Include TS/X files - `${path}/**/*.ts*`, - '--coverageDirectory', // Output to canvas/coverage - 'plugins/canvas/coverage', - ]; - } - // Mitigation for https://github.com/facebook/jest/issues/7267 - if (all || storybook) { - options = options.concat(['--no-cache', '--no-watchman']); - } + yarn jest --config x-pack/plugins/canvas/jest.config.js - if (all) { - log.info('Running all available tests. This will take a while...'); - } else if (storybook) { - path = 'plugins/canvas/storybook'; - log.info('Running Storybook Snapshot tests...'); - } else { - log.info('Running tests. This does not include Storybook Snapshots...'); - } +Provides the following additional options: + --all Runs all tests and snapshots. Slower. + --storybook Runs Storybook Snapshot tests only. +`); - if (update) { - log.info('Updating any Jest snapshots...'); - options.push('-u'); - } +if (storybookPosition > -1) { + process.argv.splice(storybookPosition, 1); - runXPackScript('jest', [path].concat(options)); - }, - { - description: ` - Jest test runner for Canvas. By default, will not include Storybook Snapshots. - `, - flags: { - boolean: ['all', 'storybook', 'update', 'coverage'], - string: ['path'], - help: ` - --all Runs all tests and snapshots. Slower. - --storybook Runs Storybook Snapshot tests only. - --update Updates Storybook Snapshot tests. - --path <string> Runs any tests at a given path. - --coverage Collect coverage statistics. - `, - }, - } -); + console.log('Running Storybook Snapshot tests only'); + process.argv.push('canvas/storybook/'); +} else if (allPosition > -1) { + process.argv.splice(allPosition, 1); + console.log('Running all available tests. This will take a while...'); +} else { + console.log('Running tests. This does not include Storybook Snapshots...'); + process.argv.push('--modulePathIgnorePatterns="/canvas/storybook/"'); +} + +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/x-pack/plugins/canvas/storybook/dll_contexts.js b/x-pack/plugins/canvas/storybook/dll_contexts.js index 02ceafd0b3983..8397f2f2e75f6 100644 --- a/x-pack/plugins/canvas/storybook/dll_contexts.js +++ b/x-pack/plugins/canvas/storybook/dll_contexts.js @@ -10,6 +10,3 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths require('../../../../src/core/server/core_app/assets/legacy_light_theme.css'); - -const json = require.context('../shareable_runtime/test/workpads', false, /\.json$/); -json.keys().forEach((key) => json(key)); diff --git a/x-pack/plugins/case/jest.config.js b/x-pack/plugins/case/jest.config.js new file mode 100644 index 0000000000000..8095c70bc4a14 --- /dev/null +++ b/x-pack/plugins/case/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/case'], +}; diff --git a/x-pack/plugins/cloud/jest.config.js b/x-pack/plugins/cloud/jest.config.js new file mode 100644 index 0000000000000..e3844a97e5692 --- /dev/null +++ b/x-pack/plugins/cloud/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/cloud'], +}; diff --git a/x-pack/plugins/code/jest.config.js b/x-pack/plugins/code/jest.config.js new file mode 100644 index 0000000000000..2b2b078cc966c --- /dev/null +++ b/x-pack/plugins/code/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/code'], +}; diff --git a/x-pack/plugins/cross_cluster_replication/jest.config.js b/x-pack/plugins/cross_cluster_replication/jest.config.js new file mode 100644 index 0000000000000..6202a45413906 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/cross_cluster_replication'], +}; diff --git a/x-pack/plugins/dashboard_enhanced/jest.config.js b/x-pack/plugins/dashboard_enhanced/jest.config.js new file mode 100644 index 0000000000000..5aeb423383c41 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/dashboard_enhanced'], +}; diff --git a/x-pack/plugins/dashboard_mode/jest.config.js b/x-pack/plugins/dashboard_mode/jest.config.js new file mode 100644 index 0000000000000..062ad302da7c4 --- /dev/null +++ b/x-pack/plugins/dashboard_mode/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/dashboard_mode'], +}; diff --git a/x-pack/plugins/data_enhanced/jest.config.js b/x-pack/plugins/data_enhanced/jest.config.js new file mode 100644 index 0000000000000..b0b1e2d94b40a --- /dev/null +++ b/x-pack/plugins/data_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/data_enhanced'], +}; diff --git a/x-pack/plugins/discover_enhanced/jest.config.js b/x-pack/plugins/discover_enhanced/jest.config.js new file mode 100644 index 0000000000000..00e040beba411 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/discover_enhanced'], +}; diff --git a/x-pack/plugins/drilldowns/jest.config.js b/x-pack/plugins/drilldowns/jest.config.js new file mode 100644 index 0000000000000..a7d79f8dac378 --- /dev/null +++ b/x-pack/plugins/drilldowns/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/drilldowns'], +}; diff --git a/x-pack/plugins/embeddable_enhanced/jest.config.js b/x-pack/plugins/embeddable_enhanced/jest.config.js new file mode 100644 index 0000000000000..c5c62f98ca2f3 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/embeddable_enhanced'], +}; diff --git a/x-pack/plugins/encrypted_saved_objects/jest.config.js b/x-pack/plugins/encrypted_saved_objects/jest.config.js new file mode 100644 index 0000000000000..0883bdb224dd0 --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/encrypted_saved_objects'], +}; diff --git a/x-pack/plugins/enterprise_search/jest.config.js b/x-pack/plugins/enterprise_search/jest.config.js new file mode 100644 index 0000000000000..db6a25a1f7efd --- /dev/null +++ b/x-pack/plugins/enterprise_search/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/enterprise_search'], +}; diff --git a/x-pack/plugins/event_log/jest.config.js b/x-pack/plugins/event_log/jest.config.js new file mode 100644 index 0000000000000..bb847d3b3c7ce --- /dev/null +++ b/x-pack/plugins/event_log/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/event_log'], +}; diff --git a/x-pack/plugins/features/jest.config.js b/x-pack/plugins/features/jest.config.js new file mode 100644 index 0000000000000..e500d35bbbd60 --- /dev/null +++ b/x-pack/plugins/features/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/features'], +}; diff --git a/x-pack/plugins/file_upload/jest.config.js b/x-pack/plugins/file_upload/jest.config.js new file mode 100644 index 0000000000000..6a042a4cc5c1e --- /dev/null +++ b/x-pack/plugins/file_upload/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/file_upload'], +}; diff --git a/x-pack/plugins/fleet/jest.config.js b/x-pack/plugins/fleet/jest.config.js new file mode 100644 index 0000000000000..521cb7467b196 --- /dev/null +++ b/x-pack/plugins/fleet/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/fleet'], +}; diff --git a/x-pack/plugins/global_search/jest.config.js b/x-pack/plugins/global_search/jest.config.js new file mode 100644 index 0000000000000..2ad904d8c57c4 --- /dev/null +++ b/x-pack/plugins/global_search/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/global_search'], +}; diff --git a/x-pack/plugins/global_search_bar/jest.config.js b/x-pack/plugins/global_search_bar/jest.config.js new file mode 100644 index 0000000000000..5b03d4a3f90d7 --- /dev/null +++ b/x-pack/plugins/global_search_bar/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/global_search_bar'], +}; diff --git a/x-pack/plugins/global_search_providers/jest.config.js b/x-pack/plugins/global_search_providers/jest.config.js new file mode 100644 index 0000000000000..3bd70206c936c --- /dev/null +++ b/x-pack/plugins/global_search_providers/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/global_search_providers'], +}; diff --git a/x-pack/plugins/graph/jest.config.js b/x-pack/plugins/graph/jest.config.js new file mode 100644 index 0000000000000..da729b0fae223 --- /dev/null +++ b/x-pack/plugins/graph/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/graph'], +}; diff --git a/x-pack/plugins/index_lifecycle_management/jest.config.js b/x-pack/plugins/index_lifecycle_management/jest.config.js new file mode 100644 index 0000000000000..906f4ff3960ae --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/index_lifecycle_management'], +}; diff --git a/x-pack/plugins/index_management/jest.config.js b/x-pack/plugins/index_management/jest.config.js new file mode 100644 index 0000000000000..d389a91675210 --- /dev/null +++ b/x-pack/plugins/index_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/index_management'], +}; diff --git a/x-pack/plugins/infra/jest.config.js b/x-pack/plugins/infra/jest.config.js new file mode 100644 index 0000000000000..507f94a2d6685 --- /dev/null +++ b/x-pack/plugins/infra/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/infra'], +}; diff --git a/x-pack/plugins/ingest_manager/jest.config.js b/x-pack/plugins/ingest_manager/jest.config.js new file mode 100644 index 0000000000000..8aff85670176b --- /dev/null +++ b/x-pack/plugins/ingest_manager/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/ingest_manager'], +}; diff --git a/x-pack/plugins/ingest_pipelines/jest.config.js b/x-pack/plugins/ingest_pipelines/jest.config.js new file mode 100644 index 0000000000000..48ce7dea0b5ba --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/ingest_pipelines'], +}; diff --git a/x-pack/plugins/lens/jest.config.js b/x-pack/plugins/lens/jest.config.js new file mode 100644 index 0000000000000..bcb80519c5ef5 --- /dev/null +++ b/x-pack/plugins/lens/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/lens'], +}; diff --git a/x-pack/plugins/license_management/jest.config.js b/x-pack/plugins/license_management/jest.config.js new file mode 100644 index 0000000000000..593b8fce47411 --- /dev/null +++ b/x-pack/plugins/license_management/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/license_management'], +}; diff --git a/x-pack/plugins/licensing/jest.config.js b/x-pack/plugins/licensing/jest.config.js new file mode 100644 index 0000000000000..72f3fd90ae5e1 --- /dev/null +++ b/x-pack/plugins/licensing/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/licensing'], +}; diff --git a/x-pack/plugins/lists/jest.config.js b/x-pack/plugins/lists/jest.config.js new file mode 100644 index 0000000000000..4d933fa20ba76 --- /dev/null +++ b/x-pack/plugins/lists/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/lists'], +}; diff --git a/x-pack/plugins/logstash/jest.config.js b/x-pack/plugins/logstash/jest.config.js new file mode 100644 index 0000000000000..52e1d7b1a6693 --- /dev/null +++ b/x-pack/plugins/logstash/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/logstash'], +}; diff --git a/x-pack/plugins/maps/jest.config.js b/x-pack/plugins/maps/jest.config.js new file mode 100644 index 0000000000000..40dea38c6f2a1 --- /dev/null +++ b/x-pack/plugins/maps/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/maps'], +}; diff --git a/x-pack/plugins/ml/jest.config.js b/x-pack/plugins/ml/jest.config.js new file mode 100644 index 0000000000000..bd77ac77c5e97 --- /dev/null +++ b/x-pack/plugins/ml/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/ml'], +}; diff --git a/x-pack/plugins/monitoring/jest.config.js b/x-pack/plugins/monitoring/jest.config.js new file mode 100644 index 0000000000000..5979afd96b477 --- /dev/null +++ b/x-pack/plugins/monitoring/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/monitoring'], +}; diff --git a/x-pack/plugins/observability/jest.config.js b/x-pack/plugins/observability/jest.config.js index cbf9a86360b89..54bb6c96ddce9 100644 --- a/x-pack/plugins/observability/jest.config.js +++ b/x-pack/plugins/observability/jest.config.js @@ -4,37 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// This is an APM-specific Jest configuration which overrides the x-pack -// configuration. It's intended for use in development and does not run in CI, -// which runs the entire x-pack suite. Run `npx jest`. - -require('../../../src/setup_node_env'); - -const { createJestConfig } = require('../../dev-tools/jest/create_jest_config'); -const { resolve } = require('path'); - -const rootDir = resolve(__dirname, '.'); -const xPackKibanaDirectory = resolve(__dirname, '../..'); -const kibanaDirectory = resolve(__dirname, '../../..'); - -const jestConfig = createJestConfig({ - kibanaDirectory, - rootDir, - xPackKibanaDirectory, -}); - module.exports = { - ...jestConfig, - reporters: ['default'], - roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], - collectCoverage: true, - collectCoverageFrom: [ - ...jestConfig.collectCoverageFrom, - '**/*.{js,mjs,jsx,ts,tsx}', - '!**/*.stories.{js,mjs,ts,tsx}', - '!**/target/**', - '!**/typings/**', - ], - coverageDirectory: `${rootDir}/target/coverage/jest`, - coverageReporters: ['html'], + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/observability'], }; diff --git a/x-pack/plugins/painless_lab/jest.config.js b/x-pack/plugins/painless_lab/jest.config.js new file mode 100644 index 0000000000000..9e0e0fe792285 --- /dev/null +++ b/x-pack/plugins/painless_lab/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/painless_lab'], +}; diff --git a/x-pack/plugins/remote_clusters/jest.config.js b/x-pack/plugins/remote_clusters/jest.config.js new file mode 100644 index 0000000000000..81728f99934bc --- /dev/null +++ b/x-pack/plugins/remote_clusters/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/remote_clusters'], +}; diff --git a/x-pack/plugins/reporting/jest.config.js b/x-pack/plugins/reporting/jest.config.js new file mode 100644 index 0000000000000..1faa533c09c7b --- /dev/null +++ b/x-pack/plugins/reporting/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/reporting'], +}; diff --git a/x-pack/plugins/rollup/jest.config.js b/x-pack/plugins/rollup/jest.config.js new file mode 100644 index 0000000000000..edb3ba860dc74 --- /dev/null +++ b/x-pack/plugins/rollup/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/rollup'], +}; diff --git a/x-pack/plugins/runtime_fields/jest.config.js b/x-pack/plugins/runtime_fields/jest.config.js new file mode 100644 index 0000000000000..9c4ec56593c8b --- /dev/null +++ b/x-pack/plugins/runtime_fields/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/runtime_fields'], +}; diff --git a/x-pack/plugins/saved_objects_tagging/jest.config.js b/x-pack/plugins/saved_objects_tagging/jest.config.js new file mode 100644 index 0000000000000..82931258a4055 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/saved_objects_tagging'], +}; diff --git a/x-pack/plugins/searchprofiler/jest.config.js b/x-pack/plugins/searchprofiler/jest.config.js new file mode 100644 index 0000000000000..b80a9924c4fd2 --- /dev/null +++ b/x-pack/plugins/searchprofiler/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/searchprofiler'], +}; diff --git a/x-pack/plugins/security/jest.config.js b/x-pack/plugins/security/jest.config.js new file mode 100644 index 0000000000000..26fee5a787850 --- /dev/null +++ b/x-pack/plugins/security/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/security'], +}; diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/jest.config.js new file mode 100644 index 0000000000000..ae7a2dbbd05ca --- /dev/null +++ b/x-pack/plugins/security_solution/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/security_solution'], +}; diff --git a/x-pack/plugins/snapshot_restore/jest.config.js b/x-pack/plugins/snapshot_restore/jest.config.js new file mode 100644 index 0000000000000..e485eff0fb355 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/snapshot_restore'], +}; diff --git a/x-pack/plugins/spaces/jest.config.js b/x-pack/plugins/spaces/jest.config.js new file mode 100644 index 0000000000000..c3e7db9a4c7c3 --- /dev/null +++ b/x-pack/plugins/spaces/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/spaces'], +}; diff --git a/x-pack/plugins/stack_alerts/jest.config.js b/x-pack/plugins/stack_alerts/jest.config.js new file mode 100644 index 0000000000000..a34c1ad828e01 --- /dev/null +++ b/x-pack/plugins/stack_alerts/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/stack_alerts'], +}; diff --git a/x-pack/plugins/task_manager/jest.config.js b/x-pack/plugins/task_manager/jest.config.js new file mode 100644 index 0000000000000..6acb44700921b --- /dev/null +++ b/x-pack/plugins/task_manager/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/task_manager'], +}; diff --git a/x-pack/plugins/telemetry_collection_xpack/jest.config.js b/x-pack/plugins/telemetry_collection_xpack/jest.config.js new file mode 100644 index 0000000000000..341be31243db8 --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/telemetry_collection_xpack'], +}; diff --git a/x-pack/plugins/transform/jest.config.js b/x-pack/plugins/transform/jest.config.js new file mode 100644 index 0000000000000..b752d071e4909 --- /dev/null +++ b/x-pack/plugins/transform/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/transform'], +}; diff --git a/x-pack/plugins/triggers_actions_ui/jest.config.js b/x-pack/plugins/triggers_actions_ui/jest.config.js new file mode 100644 index 0000000000000..63f3b24da4f56 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/triggers_actions_ui'], +}; diff --git a/x-pack/plugins/ui_actions_enhanced/jest.config.js b/x-pack/plugins/ui_actions_enhanced/jest.config.js new file mode 100644 index 0000000000000..a68fc82413583 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/ui_actions_enhanced'], +}; diff --git a/x-pack/plugins/upgrade_assistant/jest.config.js b/x-pack/plugins/upgrade_assistant/jest.config.js new file mode 100644 index 0000000000000..ad0ea1741822c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/upgrade_assistant'], +}; diff --git a/x-pack/plugins/uptime/jest.config.js b/x-pack/plugins/uptime/jest.config.js new file mode 100644 index 0000000000000..85da90927af17 --- /dev/null +++ b/x-pack/plugins/uptime/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/uptime'], +}; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js b/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js new file mode 100644 index 0000000000000..17c5c87e3ccc2 --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/vis_type_timeseries_enhanced'], +}; diff --git a/x-pack/plugins/watcher/jest.config.js b/x-pack/plugins/watcher/jest.config.js new file mode 100644 index 0000000000000..11ddd8bedc80c --- /dev/null +++ b/x-pack/plugins/watcher/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/watcher'], +}; diff --git a/x-pack/plugins/xpack_legacy/jest.config.js b/x-pack/plugins/xpack_legacy/jest.config.js new file mode 100644 index 0000000000000..16126ca0fa567 --- /dev/null +++ b/x-pack/plugins/xpack_legacy/jest.config.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['<rootDir>/x-pack/plugins/xpack_legacy'], +}; diff --git a/x-pack/scripts/jest.js b/x-pack/scripts/jest.js index f807610fd60de..68cfcf082f818 100644 --- a/x-pack/scripts/jest.js +++ b/x-pack/scripts/jest.js @@ -4,5 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -require('../../src/setup_node_env'); -require('../dev-tools/jest').runJest(); +if (process.argv.indexOf('--config') === -1) { + // append correct jest.config if none is provided + const configPath = require('path').resolve(__dirname, '../jest.config.js'); + process.argv.push('--config', configPath); + console.log('Running Jest with --config', configPath); +} + +if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; +} + +require('jest').run(); diff --git a/x-pack/scripts/jest_integration.js b/x-pack/scripts/jest_integration.js deleted file mode 100644 index 8311a9d283cbd..0000000000000 --- a/x-pack/scripts/jest_integration.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// # Run Jest integration tests -// -// All args will be forwarded directly to Jest, e.g. to watch tests run: -// -// node scripts/jest_integration --watch -// -// or to build code coverage: -// -// node scripts/jest_integration --coverage -// -// See all cli options in https://facebook.github.io/jest/docs/cli.html - -const resolve = require('path').resolve; -process.argv.push('--config', resolve(__dirname, '../test_utils/jest/config.integration.js')); -process.argv.push('--runInBand'); - -require('../../src/setup_node_env'); -require('../../src/dev/jest/cli'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap deleted file mode 100644 index 93abfaf67a009..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap +++ /dev/null @@ -1,371 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`monitor states endpoint will fetch monitor state data for the given down filters 1`] = ` -Object { - "nextPagePagination": "{\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\",\\"cursorKey\\":{\\"monitor_id\\":\\"0020-down\\"}}", - "prevPagePagination": null, - "summaries": Array [ - Object { - "histogram": Object { - "points": Array [ - Object { - "down": 1, - "timestamp": 1568172624744, - }, - Object { - "down": 2, - "timestamp": 1568172677247, - }, - Object { - "down": 1, - "timestamp": 1568172729750, - }, - Object { - "down": 2, - "timestamp": 1568172782253, - }, - Object { - "down": 2, - "timestamp": 1568172834756, - }, - Object { - "down": 2, - "timestamp": 1568172887259, - }, - Object { - "down": 1, - "timestamp": 1568172939762, - }, - Object { - "down": 2, - "timestamp": 1568172992265, - }, - Object { - "down": 2, - "timestamp": 1568173044768, - }, - Object { - "down": 2, - "timestamp": 1568173097271, - }, - Object { - "down": 1, - "timestamp": 1568173149774, - }, - Object { - "down": 2, - "timestamp": 1568173202277, - }, - ], - }, - "minInterval": 52503, - "monitor_id": "0010-down", - "state": Object { - "monitor": Object { - "name": "", - "type": "http", - }, - "observer": Object { - "geo": Object { - "name": Array [ - "mpls", - ], - }, - }, - "summary": Object { - "down": 1, - "status": "down", - "up": 0, - }, - "summaryPings": Array [ - Object { - "@timestamp": "2019-09-11T03:40:34.371Z", - "agent": Object { - "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", - "hostname": "avc-x1x", - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", - "type": "heartbeat", - "version": "8.0.0", - }, - "docId": "rZtoHm0B0I9WX_CznN_V", - "ecs": Object { - "version": "1.1.0", - }, - "error": Object { - "message": "400 Bad Request", - "type": "validate", - }, - "event": Object { - "dataset": "uptime", - }, - "host": Object { - "name": "avc-x1x", - }, - "http": Object { - "response": Object { - "body": Object { - "bytes": 3, - "content": "400", - "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", - }, - "status_code": 400, - }, - "rtt": Object { - "content": Object { - "us": 41, - }, - "response_header": Object { - "us": 36777, - }, - "total": Object { - "us": 37821, - }, - "validate": Object { - "us": 36818, - }, - "write_request": Object { - "us": 53, - }, - }, - }, - "monitor": Object { - "check_group": "d76f07d1-d445-11e9-88e3-3e80641b9c71", - "duration": Object { - "us": 37926, - }, - "id": "0010-down", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "http", - }, - "observer": Object { - "geo": Object { - "location": "37.926868, -78.024902", - "name": "mpls", - }, - "hostname": "avc-x1x", - }, - "resolve": Object { - "ip": "127.0.0.1", - "rtt": Object { - "us": 56, - }, - }, - "summary": Object { - "down": 1, - "up": 0, - }, - "tcp": Object { - "rtt": Object { - "connect": Object { - "us": 890, - }, - }, - }, - "timestamp": "2019-09-11T03:40:34.371Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - ], - "timestamp": "2019-09-11T03:40:34.371Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - }, - Object { - "histogram": Object { - "points": Array [ - Object { - "down": 1, - "timestamp": 1568172624744, - }, - Object { - "down": 2, - "timestamp": 1568172677247, - }, - Object { - "down": 1, - "timestamp": 1568172729750, - }, - Object { - "down": 2, - "timestamp": 1568172782253, - }, - Object { - "down": 2, - "timestamp": 1568172834756, - }, - Object { - "down": 2, - "timestamp": 1568172887259, - }, - Object { - "down": 1, - "timestamp": 1568172939762, - }, - Object { - "down": 2, - "timestamp": 1568172992265, - }, - Object { - "down": 2, - "timestamp": 1568173044768, - }, - Object { - "down": 2, - "timestamp": 1568173097271, - }, - Object { - "down": 1, - "timestamp": 1568173149774, - }, - Object { - "down": 2, - "timestamp": 1568173202277, - }, - ], - }, - "minInterval": 52503, - "monitor_id": "0020-down", - "state": Object { - "monitor": Object { - "name": "", - "type": "http", - }, - "observer": Object { - "geo": Object { - "name": Array [ - "mpls", - ], - }, - }, - "summary": Object { - "down": 1, - "status": "down", - "up": 0, - }, - "summaryPings": Array [ - Object { - "@timestamp": "2019-09-11T03:40:34.372Z", - "agent": Object { - "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", - "hostname": "avc-x1x", - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", - "type": "heartbeat", - "version": "8.0.0", - }, - "docId": "X5toHm0B0I9WX_CznN-6", - "ecs": Object { - "version": "1.1.0", - }, - "error": Object { - "message": "400 Bad Request", - "type": "validate", - }, - "event": Object { - "dataset": "uptime", - }, - "host": Object { - "name": "avc-x1x", - }, - "http": Object { - "response": Object { - "body": Object { - "bytes": 3, - "content": "400", - "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", - }, - "status_code": 400, - }, - "rtt": Object { - "content": Object { - "us": 54, - }, - "response_header": Object { - "us": 180, - }, - "total": Object { - "us": 555, - }, - "validate": Object { - "us": 234, - }, - "write_request": Object { - "us": 63, - }, - }, - }, - "monitor": Object { - "check_group": "d7712ecb-d445-11e9-88e3-3e80641b9c71", - "duration": Object { - "us": 14900, - }, - "id": "0020-down", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "http", - }, - "observer": Object { - "geo": Object { - "location": "37.926868, -78.024902", - "name": "mpls", - }, - "hostname": "avc-x1x", - }, - "resolve": Object { - "ip": "127.0.0.1", - "rtt": Object { - "us": 14294, - }, - }, - "summary": Object { - "down": 1, - "up": 0, - }, - "tcp": Object { - "rtt": Object { - "connect": Object { - "us": 105, - }, - }, - }, - "timestamp": "2019-09-11T03:40:34.372Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - ], - "timestamp": "2019-09-11T03:40:34.372Z", - "url": Object { - "domain": "localhost", - "full": "http://localhost:5678/pattern?r=400x1", - "path": "/pattern", - "port": 5678, - "query": "r=400x1", - "scheme": "http", - }, - }, - }, - ], - "totalSummaryCount": 2000, -} -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap deleted file mode 100644 index 434660cdc2c62..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/services/__snapshots__/throughput.snap +++ /dev/null @@ -1,250 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Throughput when data is loaded returns the service throughput has the correct throughput 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 6, - }, - Object { - "x": 1601389830000, - "y": 0, - }, - Object { - "x": 1601389860000, - "y": 0, - }, - Object { - "x": 1601389890000, - "y": 0, - }, - Object { - "x": 1601389920000, - "y": 3, - }, - Object { - "x": 1601389950000, - "y": 1, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": 0, - }, - Object { - "x": 1601390040000, - "y": 3, - }, - Object { - "x": 1601390070000, - "y": 2, - }, - Object { - "x": 1601390100000, - "y": 0, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 7, - }, - Object { - "x": 1601390190000, - "y": 3, - }, - Object { - "x": 1601390220000, - "y": 2, - }, - Object { - "x": 1601390250000, - "y": 0, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": 8, - }, - Object { - "x": 1601390340000, - "y": 0, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": 3, - }, - Object { - "x": 1601390430000, - "y": 0, - }, - Object { - "x": 1601390460000, - "y": 0, - }, - Object { - "x": 1601390490000, - "y": 0, - }, - Object { - "x": 1601390520000, - "y": 4, - }, - Object { - "x": 1601390550000, - "y": 3, - }, - Object { - "x": 1601390580000, - "y": 2, - }, - Object { - "x": 1601390610000, - "y": 0, - }, - Object { - "x": 1601390640000, - "y": 1, - }, - Object { - "x": 1601390670000, - "y": 2, - }, - Object { - "x": 1601390700000, - "y": 0, - }, - Object { - "x": 1601390730000, - "y": 0, - }, - Object { - "x": 1601390760000, - "y": 4, - }, - Object { - "x": 1601390790000, - "y": 1, - }, - Object { - "x": 1601390820000, - "y": 1, - }, - Object { - "x": 1601390850000, - "y": 0, - }, - Object { - "x": 1601390880000, - "y": 6, - }, - Object { - "x": 1601390910000, - "y": 0, - }, - Object { - "x": 1601390940000, - "y": 3, - }, - Object { - "x": 1601390970000, - "y": 0, - }, - Object { - "x": 1601391000000, - "y": 4, - }, - Object { - "x": 1601391030000, - "y": 0, - }, - Object { - "x": 1601391060000, - "y": 1, - }, - Object { - "x": 1601391090000, - "y": 0, - }, - Object { - "x": 1601391120000, - "y": 2, - }, - Object { - "x": 1601391150000, - "y": 1, - }, - Object { - "x": 1601391180000, - "y": 2, - }, - Object { - "x": 1601391210000, - "y": 0, - }, - Object { - "x": 1601391240000, - "y": 1, - }, - Object { - "x": 1601391270000, - "y": 0, - }, - Object { - "x": 1601391300000, - "y": 1, - }, - Object { - "x": 1601391330000, - "y": 0, - }, - Object { - "x": 1601391360000, - "y": 1, - }, - Object { - "x": 1601391390000, - "y": 0, - }, - Object { - "x": 1601391420000, - "y": 0, - }, - Object { - "x": 1601391450000, - "y": 0, - }, - Object { - "x": 1601391480000, - "y": 10, - }, - Object { - "x": 1601391510000, - "y": 3, - }, - Object { - "x": 1601391540000, - "y": 1, - }, - Object { - "x": 1601391570000, - "y": 0, - }, - Object { - "x": 1601391600000, - "y": 0, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap deleted file mode 100644 index 9cecb0b3b1dd7..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap +++ /dev/null @@ -1,774 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Top traces when data is loaded returns the correct buckets 1`] = ` -Array [ - Object { - "averageResponseTime": 1756, - "impact": 0, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doPost", - }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doPost", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 3251, - "impact": 0.00224063647384788, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/types", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 3813, - "impact": 0.00308293593759538, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "ResourceHttpRequestHandler", - }, - "serviceName": "opbeans-java", - "transactionName": "ResourceHttpRequestHandler", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7741, - "impact": 0.0089700396628626, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/top", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/top", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7994, - "impact": 0.00934922429689839, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "POST /api/orders", - }, - "serviceName": "opbeans-go", - "transactionName": "POST /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 10317, - "impact": 0.0128308286639543, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 10837, - "impact": 0.0136101804809449, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#topProducts", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#topProducts", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 6495, - "impact": 0.0168369967539847, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 13952, - "impact": 0.0182787976154172, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#stats", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#stats", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7324.5, - "impact": 0.0193234288008834, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customerWhoBought", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customerWhoBought", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 7089.66666666667, - "impact": 0.0292451769325711, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/customers/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/customers/:id", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 11759.5, - "impact": 0.0326173722945495, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/customers/:id", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 8109.33333333333, - "impact": 0.0338298638713675, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customer", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customer", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 8677.33333333333, - "impact": 0.0363837398255058, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#order", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#order", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 26624, - "impact": 0.0372710018940797, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/customers", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 5687.8, - "impact": 0.0399912394860756, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "averageResponseTime": 9496.33333333333, - "impact": 0.0400661771607863, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 10717.3333333333, - "impact": 0.0455561112100871, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#products", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#products", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 8438.75, - "impact": 0.04795861306131, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 17322.5, - "impact": 0.0492925036711592, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customers", - }, - "serviceName": "opbeans-java", - "transactionName": "APIRestController#customers", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 34696, - "impact": 0.0493689400993641, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 7321.4, - "impact": 0.0522330580268044, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/types/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/types/:id", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "averageResponseTime": 9663.5, - "impact": 0.0553010064294577, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::OrdersController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::OrdersController#show", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 44819, - "impact": 0.0645408217212785, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.products", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.products", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 14944, - "impact": 0.0645603055167033, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#index", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 24056, - "impact": 0.0694762169777207, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_types", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_types", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 8401.33333333333, - "impact": 0.0729173550004329, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/types", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 13182, - "impact": 0.0763944631070062, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id/customers", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 7923, - "impact": 0.0804905564066893, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::TypesController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::TypesController#index", - "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, - }, - Object { - "averageResponseTime": 19838.6666666667, - "impact": 0.0865680018257216, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::CustomersController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::CustomersController#index", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 7952.33333333333, - "impact": 0.104635475198455, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/orders/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/orders/:id", - "transactionType": "request", - "transactionsPerMinute": 0.3, - }, - Object { - "averageResponseTime": 19666, - "impact": 0.115266133732905, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api/stats", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api/stats", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 40188.5, - "impact": 0.117833498468491, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.customer", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.customer", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 26802.3333333333, - "impact": 0.117878461073318, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#show", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 14709.3333333333, - "impact": 0.129642177249393, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::StatsController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::StatsController#index", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 15432, - "impact": 0.136140772400299, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::TypesController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::TypesController#show", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 33266.3333333333, - "impact": 0.146942288833089, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.orders", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.orders", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 33445.3333333333, - "impact": 0.147747119459481, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.customers", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.customers", - "transactionType": "request", - "transactionsPerMinute": 0.1, - }, - Object { - "averageResponseTime": 107438, - "impact": 0.158391266775379, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.top_products", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.top_products", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 27696.75, - "impact": 0.163410592227497, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::ProductsController#top", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::ProductsController#top", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 55832.5, - "impact": 0.164726497795416, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.stats", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.stats", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 10483.6363636364, - "impact": 0.170204441816763, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.order", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.order", - "transactionType": "request", - "transactionsPerMinute": 0.366666666666667, - }, - Object { - "averageResponseTime": 24524.5, - "impact": 0.217905269277069, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/customers", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/customers", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 14822.3, - "impact": 0.219517928036841, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::CustomersController#show", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::CustomersController#show", - "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, - }, - Object { - "averageResponseTime": 44771.75, - "impact": 0.26577545588222, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/stats", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/stats", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 39421.4285714286, - "impact": 0.410949215592138, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Api::OrdersController#index", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Api::OrdersController#index", - "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, - }, - Object { - "averageResponseTime": 33513.3076923077, - "impact": 0.650334619948262, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products/:id", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.433333333333333, - }, - Object { - "averageResponseTime": 28933.2222222222, - "impact": 0.777916011143112, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "GET /api", - }, - "serviceName": "opbeans-node", - "transactionName": "GET /api", - "transactionType": "request", - "transactionsPerMinute": 0.6, - }, - Object { - "averageResponseTime": 101613, - "impact": 1.06341806051616, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/products/:id/customers", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.233333333333333, - }, - Object { - "averageResponseTime": 377325, - "impact": 1.12840251327172, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "GET opbeans.views.product_customers", - }, - "serviceName": "opbeans-python", - "transactionName": "GET opbeans.views.product_customers", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 39452.8333333333, - "impact": 3.54517249775948, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.sync_orders", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.sync_orders", - "transactionType": "celery", - "transactionsPerMinute": 2, - }, - Object { - "averageResponseTime": 715444.444444444, - "impact": 9.64784193809929, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/customers", - }, - "serviceName": "opbeans-rum", - "transactionName": "/customers", - "transactionType": "page-load", - "transactionsPerMinute": 0.3, - }, - Object { - "averageResponseTime": 833539.125, - "impact": 9.99152559811767, - "key": Object { - "service.name": "opbeans-go", - "transaction.name": "GET /api/orders", - }, - "serviceName": "opbeans-go", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.266666666666667, - }, - Object { - "averageResponseTime": 7480000, - "impact": 11.2080443255746, - "key": Object { - "service.name": "elastic-co-frontend", - "transaction.name": "/community/security", - }, - "serviceName": "elastic-co-frontend", - "transactionName": "/community/security", - "transactionType": "page-load", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 171383.519230769, - "impact": 13.354173900338, - "key": Object { - "service.name": "opbeans-ruby", - "transaction.name": "Rack", - }, - "serviceName": "opbeans-ruby", - "transactionName": "Rack", - "transactionType": "request", - "transactionsPerMinute": 1.73333333333333, - }, - Object { - "averageResponseTime": 1052468.6, - "impact": 15.7712781068549, - "key": Object { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doGet", - }, - "serviceName": "opbeans-java", - "transactionName": "DispatcherServlet#doGet", - "transactionType": "request", - "transactionsPerMinute": 0.333333333333333, - }, - Object { - "averageResponseTime": 1413866.66666667, - "impact": 31.7829322941256, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/products", - }, - "serviceName": "opbeans-rum", - "transactionName": "/products", - "transactionType": "page-load", - "transactionsPerMinute": 0.5, - }, - Object { - "averageResponseTime": 996583.333333333, - "impact": 35.8445542634419, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/dashboard", - }, - "serviceName": "opbeans-rum", - "transactionName": "/dashboard", - "transactionType": "page-load", - "transactionsPerMinute": 0.8, - }, - Object { - "averageResponseTime": 1046912.60465116, - "impact": 67.4671169361798, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Process completed order", - }, - "serviceName": "opbeans-node", - "transactionName": "Process completed order", - "transactionType": "Worker", - "transactionsPerMinute": 1.43333333333333, - }, - Object { - "averageResponseTime": 1142941.8, - "impact": 68.5168888461311, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Update shipping status", - }, - "serviceName": "opbeans-node", - "transactionName": "Update shipping status", - "transactionType": "Worker", - "transactionsPerMinute": 1.33333333333333, - }, - Object { - "averageResponseTime": 128285.213888889, - "impact": 69.2138167147075, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.update_stats", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.update_stats", - "transactionType": "celery", - "transactionsPerMinute": 12, - }, - Object { - "averageResponseTime": 1032979.06666667, - "impact": 69.6655125415468, - "key": Object { - "service.name": "opbeans-node", - "transaction.name": "Process payment", - }, - "serviceName": "opbeans-node", - "transactionName": "Process payment", - "transactionType": "Worker", - "transactionsPerMinute": 1.5, - }, - Object { - "averageResponseTime": 4410285.71428571, - "impact": 92.5364039355288, - "key": Object { - "service.name": "opbeans-rum", - "transaction.name": "/orders", - }, - "serviceName": "opbeans-rum", - "transactionName": "/orders", - "transactionType": "page-load", - "transactionsPerMinute": 0.466666666666667, - }, - Object { - "averageResponseTime": 1803347.81081081, - "impact": 100, - "key": Object { - "service.name": "opbeans-python", - "transaction.name": "opbeans.tasks.sync_customers", - }, - "serviceName": "opbeans-python", - "transactionName": "opbeans.tasks.sync_customers", - "transactionType": "celery", - "transactionsPerMinute": 1.23333333333333, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap deleted file mode 100644 index 5f598ba72cd72..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap +++ /dev/null @@ -1,1016 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Breakdown when data is loaded returns the transaction breakdown for a service 1`] = ` -Object { - "timeseries": Array [ - Object { - "color": "#54b399", - "data": Array [ - Object { - "x": 1601389800000, - "y": 0.0161290322580645, - }, - Object { - "x": 1601389830000, - "y": 0.402597402597403, - }, - Object { - "x": 1601389860000, - "y": 0.0303030303030303, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0.518072289156627, - }, - Object { - "x": 1601389950000, - "y": 0.120603015075377, - }, - Object { - "x": 1601389980000, - "y": 0.823529411764706, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0.273381294964029, - }, - Object { - "x": 1601390070000, - "y": 0.39047619047619, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": 0.733333333333333, - }, - Object { - "x": 1601390160000, - "y": 0.144230769230769, - }, - Object { - "x": 1601390190000, - "y": 0.0688524590163934, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 0.0540540540540541, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": 1, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 0.75, - }, - Object { - "x": 1601390460000, - "y": 0.764705882352941, - }, - Object { - "x": 1601390490000, - "y": 0.117647058823529, - }, - Object { - "x": 1601390520000, - "y": 0.220588235294118, - }, - Object { - "x": 1601390550000, - "y": 0.302325581395349, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 0.215686274509804, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.217391304347826, - }, - Object { - "x": 1601390790000, - "y": 0.253333333333333, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 0.117647058823529, - }, - Object { - "x": 1601390880000, - "y": 0.361111111111111, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 0.19047619047619, - }, - Object { - "x": 1601391000000, - "y": 0.354430379746835, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0.437956204379562, - }, - Object { - "x": 1601391150000, - "y": 0.0175438596491228, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 0.277777777777778, - }, - Object { - "x": 1601391240000, - "y": 1, - }, - Object { - "x": 1601391270000, - "y": 0.885714285714286, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0.111111111111111, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": 0.764705882352941, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0.0338983050847458, - }, - Object { - "x": 1601391510000, - "y": 0.293233082706767, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "hideLegend": false, - "legendValue": "25%", - "title": "app", - "type": "areaStacked", - }, - Object { - "color": "#6092c0", - "data": Array [ - Object { - "x": 1601389800000, - "y": 0.983870967741935, - }, - Object { - "x": 1601389830000, - "y": 0.545454545454545, - }, - Object { - "x": 1601389860000, - "y": 0.96969696969697, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0.156626506024096, - }, - Object { - "x": 1601389950000, - "y": 0.85929648241206, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0.482014388489209, - }, - Object { - "x": 1601390070000, - "y": 0.361904761904762, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 0.759615384615385, - }, - Object { - "x": 1601390190000, - "y": 0.931147540983607, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 0.945945945945946, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 0, - }, - Object { - "x": 1601390460000, - "y": 0, - }, - Object { - "x": 1601390490000, - "y": 0.784313725490196, - }, - Object { - "x": 1601390520000, - "y": 0.544117647058823, - }, - Object { - "x": 1601390550000, - "y": 0.558139534883721, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 0.784313725490196, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.536231884057971, - }, - Object { - "x": 1601390790000, - "y": 0.746666666666667, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 0.735294117647059, - }, - Object { - "x": 1601390880000, - "y": 0.416666666666667, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 0.619047619047619, - }, - Object { - "x": 1601391000000, - "y": 0.518987341772152, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0.408759124087591, - }, - Object { - "x": 1601391150000, - "y": 0.982456140350877, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 0.648148148148148, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": 0, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0.888888888888889, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": 0, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0.966101694915254, - }, - Object { - "x": 1601391510000, - "y": 0.676691729323308, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "hideLegend": false, - "legendValue": "65%", - "title": "http", - "type": "areaStacked", - }, - Object { - "color": "#d36086", - "data": Array [ - Object { - "x": 1601389800000, - "y": 0, - }, - Object { - "x": 1601389830000, - "y": 0.051948051948052, - }, - Object { - "x": 1601389860000, - "y": 0, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0.325301204819277, - }, - Object { - "x": 1601389950000, - "y": 0.0201005025125628, - }, - Object { - "x": 1601389980000, - "y": 0.176470588235294, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0.244604316546763, - }, - Object { - "x": 1601390070000, - "y": 0.247619047619048, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": 0.266666666666667, - }, - Object { - "x": 1601390160000, - "y": 0.0961538461538462, - }, - Object { - "x": 1601390190000, - "y": 0, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 0.25, - }, - Object { - "x": 1601390460000, - "y": 0.235294117647059, - }, - Object { - "x": 1601390490000, - "y": 0.0980392156862745, - }, - Object { - "x": 1601390520000, - "y": 0.235294117647059, - }, - Object { - "x": 1601390550000, - "y": 0.13953488372093, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 0, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.246376811594203, - }, - Object { - "x": 1601390790000, - "y": 0, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 0.147058823529412, - }, - Object { - "x": 1601390880000, - "y": 0.222222222222222, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 0.19047619047619, - }, - Object { - "x": 1601391000000, - "y": 0.126582278481013, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0.153284671532847, - }, - Object { - "x": 1601391150000, - "y": 0, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 0.0740740740740741, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": 0.114285714285714, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": 0.235294117647059, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0, - }, - Object { - "x": 1601391510000, - "y": 0.0300751879699248, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "hideLegend": false, - "legendValue": "10%", - "title": "postgresql", - "type": "areaStacked", - }, - ], -} -`; - -exports[`Breakdown when data is loaded returns the transaction breakdown for a transaction group 9`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 1, - }, - Object { - "x": 1601389830000, - "y": 1, - }, - Object { - "x": 1601389860000, - "y": 1, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 1, - }, - Object { - "x": 1601389950000, - "y": 1, - }, - Object { - "x": 1601389980000, - "y": null, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 1, - }, - Object { - "x": 1601390070000, - "y": 1, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": null, - }, - Object { - "x": 1601390160000, - "y": 1, - }, - Object { - "x": 1601390190000, - "y": 1, - }, - Object { - "x": 1601390220000, - "y": null, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": 1, - }, - Object { - "x": 1601390310000, - "y": null, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": null, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": null, - }, - Object { - "x": 1601390460000, - "y": null, - }, - Object { - "x": 1601390490000, - "y": 1, - }, - Object { - "x": 1601390520000, - "y": 1, - }, - Object { - "x": 1601390550000, - "y": 1, - }, - Object { - "x": 1601390580000, - "y": null, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": null, - }, - Object { - "x": 1601390670000, - "y": 1, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 1, - }, - Object { - "x": 1601390790000, - "y": 1, - }, - Object { - "x": 1601390820000, - "y": null, - }, - Object { - "x": 1601390850000, - "y": 1, - }, - Object { - "x": 1601390880000, - "y": 1, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": null, - }, - Object { - "x": 1601390970000, - "y": 1, - }, - Object { - "x": 1601391000000, - "y": 1, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": null, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 1, - }, - Object { - "x": 1601391150000, - "y": 1, - }, - Object { - "x": 1601391180000, - "y": null, - }, - Object { - "x": 1601391210000, - "y": 1, - }, - Object { - "x": 1601391240000, - "y": null, - }, - Object { - "x": 1601391270000, - "y": null, - }, - Object { - "x": 1601391300000, - "y": null, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 1, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": null, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 1, - }, - Object { - "x": 1601391510000, - "y": 1, - }, - Object { - "x": 1601391540000, - "y": null, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap deleted file mode 100644 index 1161beb7f06c0..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap +++ /dev/null @@ -1,250 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Error rate when data is loaded returns the transaction error rate has the correct error rate 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 0.166666666666667, - }, - Object { - "x": 1601389830000, - "y": null, - }, - Object { - "x": 1601389860000, - "y": null, - }, - Object { - "x": 1601389890000, - "y": null, - }, - Object { - "x": 1601389920000, - "y": 0, - }, - Object { - "x": 1601389950000, - "y": 0, - }, - Object { - "x": 1601389980000, - "y": null, - }, - Object { - "x": 1601390010000, - "y": null, - }, - Object { - "x": 1601390040000, - "y": 0, - }, - Object { - "x": 1601390070000, - "y": 0.5, - }, - Object { - "x": 1601390100000, - "y": null, - }, - Object { - "x": 1601390130000, - "y": null, - }, - Object { - "x": 1601390160000, - "y": 0.285714285714286, - }, - Object { - "x": 1601390190000, - "y": 0, - }, - Object { - "x": 1601390220000, - "y": 0, - }, - Object { - "x": 1601390250000, - "y": null, - }, - Object { - "x": 1601390280000, - "y": null, - }, - Object { - "x": 1601390310000, - "y": 0, - }, - Object { - "x": 1601390340000, - "y": null, - }, - Object { - "x": 1601390370000, - "y": null, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": null, - }, - Object { - "x": 1601390460000, - "y": null, - }, - Object { - "x": 1601390490000, - "y": null, - }, - Object { - "x": 1601390520000, - "y": 0, - }, - Object { - "x": 1601390550000, - "y": 1, - }, - Object { - "x": 1601390580000, - "y": 0, - }, - Object { - "x": 1601390610000, - "y": null, - }, - Object { - "x": 1601390640000, - "y": 1, - }, - Object { - "x": 1601390670000, - "y": 0.5, - }, - Object { - "x": 1601390700000, - "y": null, - }, - Object { - "x": 1601390730000, - "y": null, - }, - Object { - "x": 1601390760000, - "y": 0.25, - }, - Object { - "x": 1601390790000, - "y": 0, - }, - Object { - "x": 1601390820000, - "y": 0, - }, - Object { - "x": 1601390850000, - "y": null, - }, - Object { - "x": 1601390880000, - "y": 0.166666666666667, - }, - Object { - "x": 1601390910000, - "y": null, - }, - Object { - "x": 1601390940000, - "y": 0.333333333333333, - }, - Object { - "x": 1601390970000, - "y": null, - }, - Object { - "x": 1601391000000, - "y": 0, - }, - Object { - "x": 1601391030000, - "y": null, - }, - Object { - "x": 1601391060000, - "y": 1, - }, - Object { - "x": 1601391090000, - "y": null, - }, - Object { - "x": 1601391120000, - "y": 0, - }, - Object { - "x": 1601391150000, - "y": 0, - }, - Object { - "x": 1601391180000, - "y": 0, - }, - Object { - "x": 1601391210000, - "y": null, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": null, - }, - Object { - "x": 1601391300000, - "y": 0, - }, - Object { - "x": 1601391330000, - "y": null, - }, - Object { - "x": 1601391360000, - "y": 0, - }, - Object { - "x": 1601391390000, - "y": null, - }, - Object { - "x": 1601391420000, - "y": null, - }, - Object { - "x": 1601391450000, - "y": null, - }, - Object { - "x": 1601391480000, - "y": 0, - }, - Object { - "x": 1601391510000, - "y": 0, - }, - Object { - "x": 1601391540000, - "y": 1, - }, - Object { - "x": 1601391570000, - "y": null, - }, - Object { - "x": 1601391600000, - "y": null, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap deleted file mode 100644 index 9ff2294cdb08f..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap +++ /dev/null @@ -1,126 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Top transaction groups when data is loaded returns the correct buckets (when ignoring samples) 1`] = ` -Array [ - Object { - "averageResponseTime": 2292, - "impact": 0, - "key": "GET /*", - "p95": 2288, - "serviceName": "opbeans-node", - "transactionName": "GET /*", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 10317, - "impact": 0.420340829629707, - "key": "GET /api/orders/:id", - "p95": 10304, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0333333333333333, - }, - Object { - "averageResponseTime": 6495, - "impact": 0.560349681667116, - "key": "GET /api/products/:id", - "p95": 6720, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 9825.5, - "impact": 0.909245664989668, - "key": "GET /api/types", - "p95": 16496, - "serviceName": "opbeans-node", - "transactionName": "GET /api/types", - "transactionType": "request", - "transactionsPerMinute": 0.0666666666666667, - }, - Object { - "averageResponseTime": 9516.83333333333, - "impact": 2.87083620326164, - "key": "GET /api/products", - "p95": 17888, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products", - "transactionType": "request", - "transactionsPerMinute": 0.2, - }, - Object { - "averageResponseTime": 13962.2, - "impact": 3.53657227112376, - "key": "GET /api/products/:id/customers", - "p95": 23264, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/:id/customers", - "transactionType": "request", - "transactionsPerMinute": 0.166666666666667, - }, - Object { - "averageResponseTime": 21129.5, - "impact": 4.3069090413872, - "key": "GET /api/customers/:id", - "p95": 32608, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers/:id", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 10137.1111111111, - "impact": 4.65868586528666, - "key": "GET /api/orders", - "p95": 21344, - "serviceName": "opbeans-node", - "transactionName": "GET /api/orders", - "transactionType": "request", - "transactionsPerMinute": 0.3, - }, - Object { - "averageResponseTime": 24206.25, - "impact": 4.95153640465858, - "key": "GET /api/customers", - "p95": 36032, - "serviceName": "opbeans-node", - "transactionName": "GET /api/customers", - "transactionType": "request", - "transactionsPerMinute": 0.133333333333333, - }, - Object { - "averageResponseTime": 17267.0833333333, - "impact": 10.7331215479018, - "key": "GET /api/products/top", - "p95": 26208, - "serviceName": "opbeans-node", - "transactionName": "GET /api/products/top", - "transactionType": "request", - "transactionsPerMinute": 0.4, - }, - Object { - "averageResponseTime": 20417.7272727273, - "impact": 11.6439909593985, - "key": "GET /api/stats", - "p95": 24800, - "serviceName": "opbeans-node", - "transactionName": "GET /api/stats", - "transactionType": "request", - "transactionsPerMinute": 0.366666666666667, - }, - Object { - "averageResponseTime": 39822.0208333333, - "impact": 100, - "key": "GET /api", - "p95": 122816, - "serviceName": "opbeans-node", - "transactionName": "GET /api", - "transactionType": "request", - "transactionsPerMinute": 1.6, - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap deleted file mode 100644 index a75b8918ed5e4..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap +++ /dev/null @@ -1,1501 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transaction charts when data is loaded returns the correct data 4`] = ` -Object { - "apmTimeseries": Object { - "overallAvgDuration": 600888.274678112, - "responseTimes": Object { - "avg": Array [ - Object { - "x": 1601389800000, - "y": 651784.714285714, - }, - Object { - "x": 1601389830000, - "y": 747797.4, - }, - Object { - "x": 1601389860000, - "y": 567568.333333333, - }, - Object { - "x": 1601389890000, - "y": 1289936, - }, - Object { - "x": 1601389920000, - "y": 79698.6, - }, - Object { - "x": 1601389950000, - "y": 646660.833333333, - }, - Object { - "x": 1601389980000, - "y": 18095, - }, - Object { - "x": 1601390010000, - "y": 543534, - }, - Object { - "x": 1601390040000, - "y": 250234.466666667, - }, - Object { - "x": 1601390070000, - "y": 200435.2, - }, - Object { - "x": 1601390100000, - "y": 1089389.66666667, - }, - Object { - "x": 1601390130000, - "y": 1052697.33333333, - }, - Object { - "x": 1601390160000, - "y": 27908.8333333333, - }, - Object { - "x": 1601390190000, - "y": 1078058.25, - }, - Object { - "x": 1601390220000, - "y": 755843.5, - }, - Object { - "x": 1601390250000, - "y": 1371940.33333333, - }, - Object { - "x": 1601390280000, - "y": 38056, - }, - Object { - "x": 1601390310000, - "y": 1133161.33333333, - }, - Object { - "x": 1601390340000, - "y": 1236497, - }, - Object { - "x": 1601390370000, - "y": 870027, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 800475, - }, - Object { - "x": 1601390460000, - "y": 374597.2, - }, - Object { - "x": 1601390490000, - "y": 657002, - }, - Object { - "x": 1601390520000, - "y": 305164.5, - }, - Object { - "x": 1601390550000, - "y": 274576.4, - }, - Object { - "x": 1601390580000, - "y": 888533, - }, - Object { - "x": 1601390610000, - "y": 1191308, - }, - Object { - "x": 1601390640000, - "y": 1521297, - }, - Object { - "x": 1601390670000, - "y": 373994.4, - }, - Object { - "x": 1601390700000, - "y": 1108442, - }, - Object { - "x": 1601390730000, - "y": 1014666.66666667, - }, - Object { - "x": 1601390760000, - "y": 184717, - }, - Object { - "x": 1601390790000, - "y": 369595.5, - }, - Object { - "x": 1601390820000, - "y": 525805.5, - }, - Object { - "x": 1601390850000, - "y": 583359, - }, - Object { - "x": 1601390880000, - "y": 315244.25, - }, - Object { - "x": 1601390910000, - "y": 1133846, - }, - Object { - "x": 1601390940000, - "y": 312801, - }, - Object { - "x": 1601390970000, - "y": 1135768.33333333, - }, - Object { - "x": 1601391000000, - "y": 199876, - }, - Object { - "x": 1601391030000, - "y": 1508216.66666667, - }, - Object { - "x": 1601391060000, - "y": 1481690.5, - }, - Object { - "x": 1601391090000, - "y": 659469, - }, - Object { - "x": 1601391120000, - "y": 225622.666666667, - }, - Object { - "x": 1601391150000, - "y": 675812.666666667, - }, - Object { - "x": 1601391180000, - "y": 279013.333333333, - }, - Object { - "x": 1601391210000, - "y": 1327234, - }, - Object { - "x": 1601391240000, - "y": 487259, - }, - Object { - "x": 1601391270000, - "y": 686597.333333333, - }, - Object { - "x": 1601391300000, - "y": 1236063.33333333, - }, - Object { - "x": 1601391330000, - "y": 1322639, - }, - Object { - "x": 1601391360000, - "y": 517955.333333333, - }, - Object { - "x": 1601391390000, - "y": 983213.333333333, - }, - Object { - "x": 1601391420000, - "y": 920165.5, - }, - Object { - "x": 1601391450000, - "y": 655826, - }, - Object { - "x": 1601391480000, - "y": 335100.666666667, - }, - Object { - "x": 1601391510000, - "y": 496048.555555556, - }, - Object { - "x": 1601391540000, - "y": 629243, - }, - Object { - "x": 1601391570000, - "y": 796819.4, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "p95": Array [ - Object { - "x": 1601389800000, - "y": 1531888, - }, - Object { - "x": 1601389830000, - "y": 1695616, - }, - Object { - "x": 1601389860000, - "y": 1482496, - }, - Object { - "x": 1601389890000, - "y": 1617920, - }, - Object { - "x": 1601389920000, - "y": 329696, - }, - Object { - "x": 1601389950000, - "y": 1474432, - }, - Object { - "x": 1601389980000, - "y": 18048, - }, - Object { - "x": 1601390010000, - "y": 990720, - }, - Object { - "x": 1601390040000, - "y": 1163232, - }, - Object { - "x": 1601390070000, - "y": 958432, - }, - Object { - "x": 1601390100000, - "y": 1777600, - }, - Object { - "x": 1601390130000, - "y": 1873920, - }, - Object { - "x": 1601390160000, - "y": 55776, - }, - Object { - "x": 1601390190000, - "y": 1752064, - }, - Object { - "x": 1601390220000, - "y": 1136640, - }, - Object { - "x": 1601390250000, - "y": 1523712, - }, - Object { - "x": 1601390280000, - "y": 37888, - }, - Object { - "x": 1601390310000, - "y": 1196032, - }, - Object { - "x": 1601390340000, - "y": 1810304, - }, - Object { - "x": 1601390370000, - "y": 1007616, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 1523584, - }, - Object { - "x": 1601390460000, - "y": 1712096, - }, - Object { - "x": 1601390490000, - "y": 679936, - }, - Object { - "x": 1601390520000, - "y": 1163200, - }, - Object { - "x": 1601390550000, - "y": 1171392, - }, - Object { - "x": 1601390580000, - "y": 901120, - }, - Object { - "x": 1601390610000, - "y": 1355776, - }, - Object { - "x": 1601390640000, - "y": 1515520, - }, - Object { - "x": 1601390670000, - "y": 1097600, - }, - Object { - "x": 1601390700000, - "y": 1363968, - }, - Object { - "x": 1601390730000, - "y": 1290240, - }, - Object { - "x": 1601390760000, - "y": 663488, - }, - Object { - "x": 1601390790000, - "y": 827264, - }, - Object { - "x": 1601390820000, - "y": 1302400, - }, - Object { - "x": 1601390850000, - "y": 978912, - }, - Object { - "x": 1601390880000, - "y": 1482720, - }, - Object { - "x": 1601390910000, - "y": 1306624, - }, - Object { - "x": 1601390940000, - "y": 1179520, - }, - Object { - "x": 1601390970000, - "y": 1347584, - }, - Object { - "x": 1601391000000, - "y": 1122272, - }, - Object { - "x": 1601391030000, - "y": 1835008, - }, - Object { - "x": 1601391060000, - "y": 1572864, - }, - Object { - "x": 1601391090000, - "y": 1343232, - }, - Object { - "x": 1601391120000, - "y": 810880, - }, - Object { - "x": 1601391150000, - "y": 1122048, - }, - Object { - "x": 1601391180000, - "y": 782208, - }, - Object { - "x": 1601391210000, - "y": 1466368, - }, - Object { - "x": 1601391240000, - "y": 1490928, - }, - Object { - "x": 1601391270000, - "y": 1433472, - }, - Object { - "x": 1601391300000, - "y": 1677312, - }, - Object { - "x": 1601391330000, - "y": 1830912, - }, - Object { - "x": 1601391360000, - "y": 950144, - }, - Object { - "x": 1601391390000, - "y": 1265664, - }, - Object { - "x": 1601391420000, - "y": 1408896, - }, - Object { - "x": 1601391450000, - "y": 1178624, - }, - Object { - "x": 1601391480000, - "y": 946048, - }, - Object { - "x": 1601391510000, - "y": 1761248, - }, - Object { - "x": 1601391540000, - "y": 626688, - }, - Object { - "x": 1601391570000, - "y": 1564544, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - "p99": Array [ - Object { - "x": 1601389800000, - "y": 1531888, - }, - Object { - "x": 1601389830000, - "y": 1695616, - }, - Object { - "x": 1601389860000, - "y": 1482496, - }, - Object { - "x": 1601389890000, - "y": 1617920, - }, - Object { - "x": 1601389920000, - "y": 329696, - }, - Object { - "x": 1601389950000, - "y": 1474432, - }, - Object { - "x": 1601389980000, - "y": 18048, - }, - Object { - "x": 1601390010000, - "y": 990720, - }, - Object { - "x": 1601390040000, - "y": 1318880, - }, - Object { - "x": 1601390070000, - "y": 958432, - }, - Object { - "x": 1601390100000, - "y": 1777600, - }, - Object { - "x": 1601390130000, - "y": 1873920, - }, - Object { - "x": 1601390160000, - "y": 72160, - }, - Object { - "x": 1601390190000, - "y": 1752064, - }, - Object { - "x": 1601390220000, - "y": 1136640, - }, - Object { - "x": 1601390250000, - "y": 1523712, - }, - Object { - "x": 1601390280000, - "y": 37888, - }, - Object { - "x": 1601390310000, - "y": 1196032, - }, - Object { - "x": 1601390340000, - "y": 1810304, - }, - Object { - "x": 1601390370000, - "y": 1007616, - }, - Object { - "x": 1601390400000, - "y": null, - }, - Object { - "x": 1601390430000, - "y": 1523584, - }, - Object { - "x": 1601390460000, - "y": 1712096, - }, - Object { - "x": 1601390490000, - "y": 679936, - }, - Object { - "x": 1601390520000, - "y": 1163200, - }, - Object { - "x": 1601390550000, - "y": 1171392, - }, - Object { - "x": 1601390580000, - "y": 901120, - }, - Object { - "x": 1601390610000, - "y": 1355776, - }, - Object { - "x": 1601390640000, - "y": 1515520, - }, - Object { - "x": 1601390670000, - "y": 1097600, - }, - Object { - "x": 1601390700000, - "y": 1363968, - }, - Object { - "x": 1601390730000, - "y": 1290240, - }, - Object { - "x": 1601390760000, - "y": 663488, - }, - Object { - "x": 1601390790000, - "y": 827264, - }, - Object { - "x": 1601390820000, - "y": 1302400, - }, - Object { - "x": 1601390850000, - "y": 978912, - }, - Object { - "x": 1601390880000, - "y": 1482720, - }, - Object { - "x": 1601390910000, - "y": 1306624, - }, - Object { - "x": 1601390940000, - "y": 1179520, - }, - Object { - "x": 1601390970000, - "y": 1347584, - }, - Object { - "x": 1601391000000, - "y": 1122272, - }, - Object { - "x": 1601391030000, - "y": 1835008, - }, - Object { - "x": 1601391060000, - "y": 1572864, - }, - Object { - "x": 1601391090000, - "y": 1343232, - }, - Object { - "x": 1601391120000, - "y": 810880, - }, - Object { - "x": 1601391150000, - "y": 1122048, - }, - Object { - "x": 1601391180000, - "y": 782208, - }, - Object { - "x": 1601391210000, - "y": 1466368, - }, - Object { - "x": 1601391240000, - "y": 1490928, - }, - Object { - "x": 1601391270000, - "y": 1433472, - }, - Object { - "x": 1601391300000, - "y": 1677312, - }, - Object { - "x": 1601391330000, - "y": 1830912, - }, - Object { - "x": 1601391360000, - "y": 950144, - }, - Object { - "x": 1601391390000, - "y": 1265664, - }, - Object { - "x": 1601391420000, - "y": 1408896, - }, - Object { - "x": 1601391450000, - "y": 1178624, - }, - Object { - "x": 1601391480000, - "y": 946048, - }, - Object { - "x": 1601391510000, - "y": 1761248, - }, - Object { - "x": 1601391540000, - "y": 626688, - }, - Object { - "x": 1601391570000, - "y": 1564544, - }, - Object { - "x": 1601391600000, - "y": null, - }, - ], - }, - "tpmBuckets": Array [ - Object { - "avg": 3.3, - "dataPoints": Array [ - Object { - "x": 1601389800000, - "y": 6, - }, - Object { - "x": 1601389830000, - "y": 4, - }, - Object { - "x": 1601389860000, - "y": 2, - }, - Object { - "x": 1601389890000, - "y": 0, - }, - Object { - "x": 1601389920000, - "y": 8, - }, - Object { - "x": 1601389950000, - "y": 2, - }, - Object { - "x": 1601389980000, - "y": 2, - }, - Object { - "x": 1601390010000, - "y": 0, - }, - Object { - "x": 1601390040000, - "y": 22, - }, - Object { - "x": 1601390070000, - "y": 8, - }, - Object { - "x": 1601390100000, - "y": 2, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 20, - }, - Object { - "x": 1601390190000, - "y": 2, - }, - Object { - "x": 1601390220000, - "y": 0, - }, - Object { - "x": 1601390250000, - "y": 0, - }, - Object { - "x": 1601390280000, - "y": 2, - }, - Object { - "x": 1601390310000, - "y": 0, - }, - Object { - "x": 1601390340000, - "y": 2, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": 2, - }, - Object { - "x": 1601390460000, - "y": 8, - }, - Object { - "x": 1601390490000, - "y": 0, - }, - Object { - "x": 1601390520000, - "y": 6, - }, - Object { - "x": 1601390550000, - "y": 6, - }, - Object { - "x": 1601390580000, - "y": 0, - }, - Object { - "x": 1601390610000, - "y": 0, - }, - Object { - "x": 1601390640000, - "y": 0, - }, - Object { - "x": 1601390670000, - "y": 4, - }, - Object { - "x": 1601390700000, - "y": 0, - }, - Object { - "x": 1601390730000, - "y": 0, - }, - Object { - "x": 1601390760000, - "y": 4, - }, - Object { - "x": 1601390790000, - "y": 4, - }, - Object { - "x": 1601390820000, - "y": 6, - }, - Object { - "x": 1601390850000, - "y": 2, - }, - Object { - "x": 1601390880000, - "y": 12, - }, - Object { - "x": 1601390910000, - "y": 0, - }, - Object { - "x": 1601390940000, - "y": 6, - }, - Object { - "x": 1601390970000, - "y": 0, - }, - Object { - "x": 1601391000000, - "y": 10, - }, - Object { - "x": 1601391030000, - "y": 0, - }, - Object { - "x": 1601391060000, - "y": 0, - }, - Object { - "x": 1601391090000, - "y": 2, - }, - Object { - "x": 1601391120000, - "y": 8, - }, - Object { - "x": 1601391150000, - "y": 2, - }, - Object { - "x": 1601391180000, - "y": 4, - }, - Object { - "x": 1601391210000, - "y": 0, - }, - Object { - "x": 1601391240000, - "y": 6, - }, - Object { - "x": 1601391270000, - "y": 2, - }, - Object { - "x": 1601391300000, - "y": 0, - }, - Object { - "x": 1601391330000, - "y": 0, - }, - Object { - "x": 1601391360000, - "y": 2, - }, - Object { - "x": 1601391390000, - "y": 0, - }, - Object { - "x": 1601391420000, - "y": 2, - }, - Object { - "x": 1601391450000, - "y": 0, - }, - Object { - "x": 1601391480000, - "y": 4, - }, - Object { - "x": 1601391510000, - "y": 12, - }, - Object { - "x": 1601391540000, - "y": 0, - }, - Object { - "x": 1601391570000, - "y": 2, - }, - Object { - "x": 1601391600000, - "y": 0, - }, - ], - "key": "HTTP 2xx", - }, - Object { - "avg": 0.2, - "dataPoints": Array [ - Object { - "x": 1601389800000, - "y": 0, - }, - Object { - "x": 1601389830000, - "y": 0, - }, - Object { - "x": 1601389860000, - "y": 0, - }, - Object { - "x": 1601389890000, - "y": 0, - }, - Object { - "x": 1601389920000, - "y": 0, - }, - Object { - "x": 1601389950000, - "y": 2, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": 0, - }, - Object { - "x": 1601390040000, - "y": 2, - }, - Object { - "x": 1601390070000, - "y": 0, - }, - Object { - "x": 1601390100000, - "y": 0, - }, - Object { - "x": 1601390130000, - "y": 0, - }, - Object { - "x": 1601390160000, - "y": 4, - }, - Object { - "x": 1601390190000, - "y": 0, - }, - Object { - "x": 1601390220000, - "y": 0, - }, - Object { - "x": 1601390250000, - "y": 0, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": 0, - }, - Object { - "x": 1601390340000, - "y": 0, - }, - Object { - "x": 1601390370000, - "y": 0, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": 0, - }, - Object { - "x": 1601390460000, - "y": 0, - }, - Object { - "x": 1601390490000, - "y": 0, - }, - Object { - "x": 1601390520000, - "y": 0, - }, - Object { - "x": 1601390550000, - "y": 0, - }, - Object { - "x": 1601390580000, - "y": 0, - }, - Object { - "x": 1601390610000, - "y": 0, - }, - Object { - "x": 1601390640000, - "y": 0, - }, - Object { - "x": 1601390670000, - "y": 2, - }, - Object { - "x": 1601390700000, - "y": 0, - }, - Object { - "x": 1601390730000, - "y": 0, - }, - Object { - "x": 1601390760000, - "y": 2, - }, - Object { - "x": 1601390790000, - "y": 0, - }, - Object { - "x": 1601390820000, - "y": 0, - }, - Object { - "x": 1601390850000, - "y": 0, - }, - Object { - "x": 1601390880000, - "y": 0, - }, - Object { - "x": 1601390910000, - "y": 0, - }, - Object { - "x": 1601390940000, - "y": 0, - }, - Object { - "x": 1601390970000, - "y": 0, - }, - Object { - "x": 1601391000000, - "y": 0, - }, - Object { - "x": 1601391030000, - "y": 0, - }, - Object { - "x": 1601391060000, - "y": 0, - }, - Object { - "x": 1601391090000, - "y": 0, - }, - Object { - "x": 1601391120000, - "y": 0, - }, - Object { - "x": 1601391150000, - "y": 0, - }, - Object { - "x": 1601391180000, - "y": 0, - }, - Object { - "x": 1601391210000, - "y": 0, - }, - Object { - "x": 1601391240000, - "y": 0, - }, - Object { - "x": 1601391270000, - "y": 0, - }, - Object { - "x": 1601391300000, - "y": 0, - }, - Object { - "x": 1601391330000, - "y": 0, - }, - Object { - "x": 1601391360000, - "y": 0, - }, - Object { - "x": 1601391390000, - "y": 0, - }, - Object { - "x": 1601391420000, - "y": 0, - }, - Object { - "x": 1601391450000, - "y": 0, - }, - Object { - "x": 1601391480000, - "y": 0, - }, - Object { - "x": 1601391510000, - "y": 0, - }, - Object { - "x": 1601391540000, - "y": 0, - }, - Object { - "x": 1601391570000, - "y": 0, - }, - Object { - "x": 1601391600000, - "y": 0, - }, - ], - "key": "HTTP 4xx", - }, - Object { - "avg": 4.26666666666667, - "dataPoints": Array [ - Object { - "x": 1601389800000, - "y": 8, - }, - Object { - "x": 1601389830000, - "y": 6, - }, - Object { - "x": 1601389860000, - "y": 4, - }, - Object { - "x": 1601389890000, - "y": 4, - }, - Object { - "x": 1601389920000, - "y": 2, - }, - Object { - "x": 1601389950000, - "y": 8, - }, - Object { - "x": 1601389980000, - "y": 0, - }, - Object { - "x": 1601390010000, - "y": 6, - }, - Object { - "x": 1601390040000, - "y": 6, - }, - Object { - "x": 1601390070000, - "y": 2, - }, - Object { - "x": 1601390100000, - "y": 4, - }, - Object { - "x": 1601390130000, - "y": 6, - }, - Object { - "x": 1601390160000, - "y": 0, - }, - Object { - "x": 1601390190000, - "y": 6, - }, - Object { - "x": 1601390220000, - "y": 4, - }, - Object { - "x": 1601390250000, - "y": 6, - }, - Object { - "x": 1601390280000, - "y": 0, - }, - Object { - "x": 1601390310000, - "y": 6, - }, - Object { - "x": 1601390340000, - "y": 6, - }, - Object { - "x": 1601390370000, - "y": 4, - }, - Object { - "x": 1601390400000, - "y": 0, - }, - Object { - "x": 1601390430000, - "y": 6, - }, - Object { - "x": 1601390460000, - "y": 2, - }, - Object { - "x": 1601390490000, - "y": 6, - }, - Object { - "x": 1601390520000, - "y": 2, - }, - Object { - "x": 1601390550000, - "y": 4, - }, - Object { - "x": 1601390580000, - "y": 4, - }, - Object { - "x": 1601390610000, - "y": 4, - }, - Object { - "x": 1601390640000, - "y": 2, - }, - Object { - "x": 1601390670000, - "y": 4, - }, - Object { - "x": 1601390700000, - "y": 4, - }, - Object { - "x": 1601390730000, - "y": 6, - }, - Object { - "x": 1601390760000, - "y": 2, - }, - Object { - "x": 1601390790000, - "y": 4, - }, - Object { - "x": 1601390820000, - "y": 6, - }, - Object { - "x": 1601390850000, - "y": 4, - }, - Object { - "x": 1601390880000, - "y": 4, - }, - Object { - "x": 1601390910000, - "y": 8, - }, - Object { - "x": 1601390940000, - "y": 2, - }, - Object { - "x": 1601390970000, - "y": 6, - }, - Object { - "x": 1601391000000, - "y": 2, - }, - Object { - "x": 1601391030000, - "y": 6, - }, - Object { - "x": 1601391060000, - "y": 4, - }, - Object { - "x": 1601391090000, - "y": 6, - }, - Object { - "x": 1601391120000, - "y": 4, - }, - Object { - "x": 1601391150000, - "y": 4, - }, - Object { - "x": 1601391180000, - "y": 2, - }, - Object { - "x": 1601391210000, - "y": 4, - }, - Object { - "x": 1601391240000, - "y": 4, - }, - Object { - "x": 1601391270000, - "y": 4, - }, - Object { - "x": 1601391300000, - "y": 6, - }, - Object { - "x": 1601391330000, - "y": 4, - }, - Object { - "x": 1601391360000, - "y": 4, - }, - Object { - "x": 1601391390000, - "y": 6, - }, - Object { - "x": 1601391420000, - "y": 6, - }, - Object { - "x": 1601391450000, - "y": 4, - }, - Object { - "x": 1601391480000, - "y": 2, - }, - Object { - "x": 1601391510000, - "y": 6, - }, - Object { - "x": 1601391540000, - "y": 2, - }, - Object { - "x": 1601391570000, - "y": 8, - }, - Object { - "x": 1601391600000, - "y": 0, - }, - ], - "key": "success", - }, - ], - }, -} -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap deleted file mode 100644 index 4bf242d8f9b6d..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_load_dist.snap +++ /dev/null @@ -1,824 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UX page load dist when there is data returns page load distribution 1`] = ` -Object { - "maxDuration": 54.46, - "minDuration": 0, - "pageLoadDistribution": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 16.6666666666667, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 50, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 16.6666666666667, - }, - Object { - "x": 38, - "y": 0, - }, - Object { - "x": 38.5, - "y": 0, - }, - Object { - "x": 39, - "y": 0, - }, - Object { - "x": 39.5, - "y": 0, - }, - Object { - "x": 40, - "y": 0, - }, - Object { - "x": 40.5, - "y": 0, - }, - Object { - "x": 41, - "y": 0, - }, - Object { - "x": 41.5, - "y": 0, - }, - Object { - "x": 42, - "y": 0, - }, - Object { - "x": 42.5, - "y": 0, - }, - Object { - "x": 43, - "y": 0, - }, - Object { - "x": 43.5, - "y": 0, - }, - Object { - "x": 44, - "y": 0, - }, - Object { - "x": 44.5, - "y": 0, - }, - Object { - "x": 45, - "y": 0, - }, - Object { - "x": 45.5, - "y": 0, - }, - Object { - "x": 46, - "y": 0, - }, - Object { - "x": 46.5, - "y": 0, - }, - Object { - "x": 47, - "y": 0, - }, - Object { - "x": 47.5, - "y": 0, - }, - Object { - "x": 48, - "y": 0, - }, - Object { - "x": 48.5, - "y": 0, - }, - Object { - "x": 49, - "y": 0, - }, - Object { - "x": 49.5, - "y": 0, - }, - Object { - "x": 50, - "y": 0, - }, - Object { - "x": 50.5, - "y": 0, - }, - Object { - "x": 51, - "y": 0, - }, - Object { - "x": 51.5, - "y": 0, - }, - Object { - "x": 52, - "y": 0, - }, - Object { - "x": 52.5, - "y": 0, - }, - Object { - "x": 53, - "y": 0, - }, - Object { - "x": 53.5, - "y": 0, - }, - Object { - "x": 54, - "y": 0, - }, - Object { - "x": 54.5, - "y": 16.6666666666667, - }, - ], - "percentiles": Object { - "50.0": 4.88, - "75.0": 37.09, - "90.0": 37.09, - "95.0": 54.46, - "99.0": 54.46, - }, -} -`; - -exports[`UX page load dist when there is data returns page load distribution with breakdown 1`] = ` -Array [ - Object { - "data": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 25, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 25, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 25, - }, - ], - "name": "Chrome", - }, - Object { - "data": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 0, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 100, - }, - ], - "name": "Chrome Mobile", - }, -] -`; - -exports[`UX page load dist when there is no data returns empty list 1`] = `Object {}`; - -exports[`UX page load dist when there is no data returns empty list with breakdowns 1`] = `Object {}`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap deleted file mode 100644 index 38b009fc73d34..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap +++ /dev/null @@ -1,280 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CSM page views when there is data returns page views 1`] = ` -Object { - "items": Array [ - Object { - "x": 1600149947000, - "y": 1, - }, - Object { - "x": 1600149957000, - "y": 0, - }, - Object { - "x": 1600149967000, - "y": 0, - }, - Object { - "x": 1600149977000, - "y": 0, - }, - Object { - "x": 1600149987000, - "y": 0, - }, - Object { - "x": 1600149997000, - "y": 0, - }, - Object { - "x": 1600150007000, - "y": 0, - }, - Object { - "x": 1600150017000, - "y": 0, - }, - Object { - "x": 1600150027000, - "y": 1, - }, - Object { - "x": 1600150037000, - "y": 0, - }, - Object { - "x": 1600150047000, - "y": 0, - }, - Object { - "x": 1600150057000, - "y": 0, - }, - Object { - "x": 1600150067000, - "y": 0, - }, - Object { - "x": 1600150077000, - "y": 1, - }, - Object { - "x": 1600150087000, - "y": 0, - }, - Object { - "x": 1600150097000, - "y": 0, - }, - Object { - "x": 1600150107000, - "y": 0, - }, - Object { - "x": 1600150117000, - "y": 0, - }, - Object { - "x": 1600150127000, - "y": 0, - }, - Object { - "x": 1600150137000, - "y": 0, - }, - Object { - "x": 1600150147000, - "y": 0, - }, - Object { - "x": 1600150157000, - "y": 0, - }, - Object { - "x": 1600150167000, - "y": 0, - }, - Object { - "x": 1600150177000, - "y": 1, - }, - Object { - "x": 1600150187000, - "y": 0, - }, - Object { - "x": 1600150197000, - "y": 0, - }, - Object { - "x": 1600150207000, - "y": 1, - }, - Object { - "x": 1600150217000, - "y": 0, - }, - Object { - "x": 1600150227000, - "y": 0, - }, - Object { - "x": 1600150237000, - "y": 1, - }, - ], - "topItems": Array [], -} -`; - -exports[`CSM page views when there is data returns page views with breakdown 1`] = ` -Object { - "items": Array [ - Object { - "Chrome": 1, - "x": 1600149947000, - "y": 1, - }, - Object { - "x": 1600149957000, - "y": 0, - }, - Object { - "x": 1600149967000, - "y": 0, - }, - Object { - "x": 1600149977000, - "y": 0, - }, - Object { - "x": 1600149987000, - "y": 0, - }, - Object { - "x": 1600149997000, - "y": 0, - }, - Object { - "x": 1600150007000, - "y": 0, - }, - Object { - "x": 1600150017000, - "y": 0, - }, - Object { - "Chrome": 1, - "x": 1600150027000, - "y": 1, - }, - Object { - "x": 1600150037000, - "y": 0, - }, - Object { - "x": 1600150047000, - "y": 0, - }, - Object { - "x": 1600150057000, - "y": 0, - }, - Object { - "x": 1600150067000, - "y": 0, - }, - Object { - "Chrome": 1, - "x": 1600150077000, - "y": 1, - }, - Object { - "x": 1600150087000, - "y": 0, - }, - Object { - "x": 1600150097000, - "y": 0, - }, - Object { - "x": 1600150107000, - "y": 0, - }, - Object { - "x": 1600150117000, - "y": 0, - }, - Object { - "x": 1600150127000, - "y": 0, - }, - Object { - "x": 1600150137000, - "y": 0, - }, - Object { - "x": 1600150147000, - "y": 0, - }, - Object { - "x": 1600150157000, - "y": 0, - }, - Object { - "x": 1600150167000, - "y": 0, - }, - Object { - "Chrome": 1, - "x": 1600150177000, - "y": 1, - }, - Object { - "x": 1600150187000, - "y": 0, - }, - Object { - "x": 1600150197000, - "y": 0, - }, - Object { - "Chrome Mobile": 1, - "x": 1600150207000, - "y": 1, - }, - Object { - "x": 1600150217000, - "y": 0, - }, - Object { - "x": 1600150227000, - "y": 0, - }, - Object { - "Chrome Mobile": 1, - "x": 1600150237000, - "y": 1, - }, - ], - "topItems": Array [ - "Chrome", - "Chrome Mobile", - ], -} -`; - -exports[`CSM page views when there is no data returns empty list 1`] = ` -Object { - "items": Array [], - "topItems": Array [], -} -`; - -exports[`CSM page views when there is no data returns empty list with breakdowns 1`] = ` -Object { - "items": Array [], - "topItems": Array [], -} -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap deleted file mode 100644 index a7e6ae03b1bdc..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ /dev/null @@ -1,1995 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Service Maps with a trial license /api/apm/service-map when there is data returns service map elements filtering by environment not defined 1`] = ` -Object { - "elements": Array [ - Object { - "data": Object { - "agent.name": "rum-js", - "id": "elastic-co-frontend", - "service.environment": "ENVIRONMENT_NOT_DEFINED", - "service.name": "elastic-co-frontend", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "groupedConnections": Array [ - Object { - "id": ">a18132920325.cdn.optimizely.com:443", - "label": "a18132920325.cdn.optimizely.com:443", - "span.destination.service.resource": "a18132920325.cdn.optimizely.com:443", - "span.subtype": "iframe", - "span.type": "resource", - }, - Object { - "id": ">cdn.optimizely.com:443", - "label": "cdn.optimizely.com:443", - "span.destination.service.resource": "cdn.optimizely.com:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">fonts.googleapis.com:443", - "label": "fonts.googleapis.com:443", - "span.destination.service.resource": "fonts.googleapis.com:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">images.contentstack.io:443", - "label": "images.contentstack.io:443", - "span.destination.service.resource": "images.contentstack.io:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">info.elastic.co:443", - "label": "info.elastic.co:443", - "span.destination.service.resource": "info.elastic.co:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">p.typekit.net:443", - "label": "p.typekit.net:443", - "span.destination.service.resource": "p.typekit.net:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">static-www.elastic.co:443", - "label": "static-www.elastic.co:443", - "span.destination.service.resource": "static-www.elastic.co:443", - "span.subtype": "img", - "span.type": "resource", - }, - Object { - "id": ">use.typekit.net:443", - "label": "use.typekit.net:443", - "span.destination.service.resource": "use.typekit.net:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">www.elastic.co:443", - "label": "www.elastic.co:443", - "span.destination.service.resource": "www.elastic.co:443", - "span.subtype": "browser-timing", - "span.type": "hard-navigation", - }, - ], - "id": "resourceGroup{elastic-co-frontend}", - "label": "9 resources", - "span.type": "external", - }, - }, - Object { - "data": Object { - "id": "elastic-co-frontend~>resourceGroup{elastic-co-frontend}", - "source": "elastic-co-frontend", - "target": "resourceGroup{elastic-co-frontend}", - }, - }, - ], -} -`; - -exports[`Service Maps with a trial license /api/apm/service-map when there is data returns the correct data 3`] = ` -Array [ - Object { - "data": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "elastic-co-frontend", - "service.name": "elastic-co-frontend", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - Object { - "data": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - Object { - "data": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - Object { - "data": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "dotnet", - "id": "opbeans-dotnet", - "service.environment": null, - "service.name": "opbeans-dotnet", - }, - }, - Object { - "data": Object { - "groupedConnections": Array [ - Object { - "id": ">a18132920325.cdn.optimizely.com:443", - "label": "a18132920325.cdn.optimizely.com:443", - "span.destination.service.resource": "a18132920325.cdn.optimizely.com:443", - "span.subtype": "iframe", - "span.type": "resource", - }, - Object { - "id": ">cdn.optimizely.com:443", - "label": "cdn.optimizely.com:443", - "span.destination.service.resource": "cdn.optimizely.com:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">fonts.googleapis.com:443", - "label": "fonts.googleapis.com:443", - "span.destination.service.resource": "fonts.googleapis.com:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">images.contentstack.io:443", - "label": "images.contentstack.io:443", - "span.destination.service.resource": "images.contentstack.io:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">info.elastic.co:443", - "label": "info.elastic.co:443", - "span.destination.service.resource": "info.elastic.co:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">p.typekit.net:443", - "label": "p.typekit.net:443", - "span.destination.service.resource": "p.typekit.net:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">static-www.elastic.co:443", - "label": "static-www.elastic.co:443", - "span.destination.service.resource": "static-www.elastic.co:443", - "span.subtype": "img", - "span.type": "resource", - }, - Object { - "id": ">use.typekit.net:443", - "label": "use.typekit.net:443", - "span.destination.service.resource": "use.typekit.net:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">www.elastic.co:443", - "label": "www.elastic.co:443", - "span.destination.service.resource": "www.elastic.co:443", - "span.subtype": "browser-timing", - "span.type": "hard-navigation", - }, - ], - "id": "resourceGroup{elastic-co-frontend}", - "label": "9 resources", - "span.type": "external", - }, - }, - Object { - "data": Object { - "id": "opbeans-go~>postgresql", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-node", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-python", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-java~>postgresql", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-node", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-ruby", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>postgresql", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-python", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-ruby", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>elasticsearch", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">elasticsearch", - "targetData": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>postgresql", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>redis", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">redis", - "targetData": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-python~opbeans-ruby", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~>postgresql", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-go", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-python", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-go", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-java", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-node", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-python", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-ruby", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "elastic-co-frontend~>resourceGroup{elastic-co-frontend}", - "source": "elastic-co-frontend", - "target": "resourceGroup{elastic-co-frontend}", - }, - }, -] -`; - -exports[`Service Maps with a trial license when there is data with anomalies with the default apm user returns the correct anomaly stats 3`] = ` -Object { - "elements": Array [ - Object { - "data": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "elastic-co-frontend", - "service.name": "elastic-co-frontend", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - }, - Object { - "data": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - Object { - "data": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - Object { - "data": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - Object { - "data": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - Object { - "data": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - Object { - "data": Object { - "agent.name": "dotnet", - "id": "opbeans-dotnet", - "service.environment": null, - "service.name": "opbeans-dotnet", - }, - }, - Object { - "data": Object { - "groupedConnections": Array [ - Object { - "id": ">a18132920325.cdn.optimizely.com:443", - "label": "a18132920325.cdn.optimizely.com:443", - "span.destination.service.resource": "a18132920325.cdn.optimizely.com:443", - "span.subtype": "iframe", - "span.type": "resource", - }, - Object { - "id": ">cdn.optimizely.com:443", - "label": "cdn.optimizely.com:443", - "span.destination.service.resource": "cdn.optimizely.com:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">fonts.googleapis.com:443", - "label": "fonts.googleapis.com:443", - "span.destination.service.resource": "fonts.googleapis.com:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">images.contentstack.io:443", - "label": "images.contentstack.io:443", - "span.destination.service.resource": "images.contentstack.io:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">info.elastic.co:443", - "label": "info.elastic.co:443", - "span.destination.service.resource": "info.elastic.co:443", - "span.subtype": "script", - "span.type": "resource", - }, - Object { - "id": ">p.typekit.net:443", - "label": "p.typekit.net:443", - "span.destination.service.resource": "p.typekit.net:443", - "span.subtype": "css", - "span.type": "resource", - }, - Object { - "id": ">static-www.elastic.co:443", - "label": "static-www.elastic.co:443", - "span.destination.service.resource": "static-www.elastic.co:443", - "span.subtype": "img", - "span.type": "resource", - }, - Object { - "id": ">use.typekit.net:443", - "label": "use.typekit.net:443", - "span.destination.service.resource": "use.typekit.net:443", - "span.subtype": "link", - "span.type": "resource", - }, - Object { - "id": ">www.elastic.co:443", - "label": "www.elastic.co:443", - "span.destination.service.resource": "www.elastic.co:443", - "span.subtype": "browser-timing", - "span.type": "hard-navigation", - }, - ], - "id": "resourceGroup{elastic-co-frontend}", - "label": "9 resources", - "span.type": "external", - }, - }, - Object { - "data": Object { - "id": "opbeans-go~>postgresql", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-node", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-go~opbeans-python", - "source": "opbeans-go", - "sourceData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-java~>postgresql", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-node", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-java~opbeans-ruby", - "source": "opbeans-java", - "sourceData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~>postgresql", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-node~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-python", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-node~opbeans-ruby", - "source": "opbeans-node", - "sourceData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>elasticsearch", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">elasticsearch", - "targetData": Object { - "id": ">elasticsearch", - "label": "elasticsearch", - "span.destination.service.resource": "elasticsearch", - "span.subtype": "elasticsearch", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>postgresql", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~>redis", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">redis", - "targetData": Object { - "id": ">redis", - "label": "redis", - "span.destination.service.resource": "redis", - "span.subtype": "redis", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-go", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-python~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "bidirectional": true, - "id": "opbeans-python~opbeans-ruby", - "source": "opbeans-python", - "sourceData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~>postgresql", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": ">postgresql", - "targetData": Object { - "id": ">postgresql", - "label": "postgresql", - "span.destination.service.resource": "postgresql", - "span.subtype": "postgresql", - "span.type": "db", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-go", - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-java", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-node", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-ruby~opbeans-python", - "isInverseEdge": true, - "source": "opbeans-ruby", - "sourceData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-go", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-go", - "targetData": Object { - "agent.name": "go", - "id": "opbeans-go", - "service.environment": "testing", - "service.name": "opbeans-go", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-java", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-java", - "targetData": Object { - "agent.name": "java", - "id": "opbeans-java", - "service.environment": "production", - "service.name": "opbeans-java", - "serviceAnomalyStats": Object { - "actualValue": 559010.6, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-node", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-node", - "targetData": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-python", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-python", - "targetData": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.environment": "production", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 47107.7692307692, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "opbeans-rum~opbeans-ruby", - "source": "opbeans-rum", - "sourceData": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-7ed6-high_mean_transaction_duration", - "transactionType": "page-load", - }, - }, - "target": "opbeans-ruby", - "targetData": Object { - "agent.name": "ruby", - "id": "opbeans-ruby", - "service.environment": "production", - "service.name": "opbeans-ruby", - "serviceAnomalyStats": Object { - "actualValue": 141536.936507937, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-production-229a-high_mean_transaction_duration", - "transactionType": "request", - }, - }, - }, - }, - Object { - "data": Object { - "id": "elastic-co-frontend~>resourceGroup{elastic-co-frontend}", - "source": "elastic-co-frontend", - "target": "resourceGroup{elastic-co-frontend}", - }, - }, - ], -} -`; diff --git a/x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap b/x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap deleted file mode 100644 index 8169e73202fbc..0000000000000 --- a/x-pack/test/apm_api_integration/trial/tests/services/__snapshots__/transaction_groups_charts.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`APM Transaction Overview when data is loaded and fetching transaction groups charts with uiFilters when not defined environments selected should return the correct anomaly boundaries 1`] = `Array []`; - -exports[`APM Transaction Overview when data is loaded and fetching transaction groups charts with uiFilters with environment selected and empty kuery filter should return a non-empty anomaly series 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 1206111.33487531, - "y0": 10555.1290143587, - }, - Object { - "x": 1601390700000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, - Object { - "x": 1601391600000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, -] -`; - -exports[`APM Transaction Overview when data is loaded and fetching transaction groups charts with uiFilters with environment selected in uiFilters should return a non-empty anomaly series 1`] = ` -Array [ - Object { - "x": 1601389800000, - "y": 1206111.33487531, - "y0": 10555.1290143587, - }, - Object { - "x": 1601390700000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, - Object { - "x": 1601391600000, - "y": 1223987.49321778, - "y0": 10177.4677901726, - }, -] -`; From 4f3d72b413505cfa9e4475e7b02d3a46fff85ef9 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger <scotty.bollinger@elastic.co> Date: Wed, 2 Dec 2020 15:34:19 -0600 Subject: [PATCH 073/107] [Enterprise Search] Move schema types to shared (#84822) * Move schema types to shared We use the Schema types in Workplace Search as well, so moving these to shared. Also, we have a component called IndexingStatus so reverting to the prefixed IIndexingStatus interface name * Fix misspelled interface --- .../components/engine/engine_logic.ts | 4 +-- .../app_search/components/engine/types.ts | 4 +-- .../app_search/components/schema/types.ts | 34 ------------------- .../public/applications/shared/types.ts | 23 +++++++++++++ 4 files changed, 27 insertions(+), 38 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index 2e7595e3ee87b..51896becd8703 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -8,7 +8,7 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; -import { IndexingStatus } from '../schema/types'; +import { IIndexingStatus } from '../../../shared/types'; import { EngineDetails } from './types'; interface EngineValues { @@ -25,7 +25,7 @@ interface EngineValues { interface EngineActions { setEngineData(engine: EngineDetails): { engine: EngineDetails }; setEngineName(engineName: string): { engineName: string }; - setIndexingStatus(activeReindexJob: IndexingStatus): { activeReindexJob: IndexingStatus }; + setIndexingStatus(activeReindexJob: IIndexingStatus): { activeReindexJob: IIndexingStatus }; setEngineNotFound(notFound: boolean): { notFound: boolean }; clearEngine(): void; initializeEngine(): void; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index 635d1136291aa..99ad19fea0619 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -5,7 +5,7 @@ */ import { ApiToken } from '../credentials/types'; -import { Schema, SchemaConflicts, IndexingStatus } from '../schema/types'; +import { Schema, SchemaConflicts, IIndexingStatus } from '../../../shared/types'; export interface Engine { name: string; @@ -26,7 +26,7 @@ export interface EngineDetails extends Engine { schema: Schema; schemaConflicts?: SchemaConflicts; unconfirmedFields?: string[]; - activeReindexJob?: IndexingStatus; + activeReindexJob?: IIndexingStatus; invalidBoosts: boolean; sample?: boolean; isMeta: boolean; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts deleted file mode 100644 index 84f402dd3b95f..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; - -export interface Schema { - [key: string]: SchemaTypes; -} - -// this is a mapping of schema field types ("string", "number", "geolocation", "date") to the names -// of source engines which utilize that type -export type SchemaConflictFieldTypes = { - [key in SchemaTypes]: string[]; -}; - -export interface SchemaConflict { - fieldTypes: SchemaConflictFieldTypes; - resolution?: string; -} - -// For now these values are ISchemaConflictFieldTypes, but in the near future will be ISchemaConflict -// once we implement schema conflict resolution -export interface SchemaConflicts { - [key: string]: SchemaConflictFieldTypes; -} - -export interface IndexingStatus { - percentageComplete: number; - numDocumentsWithErrors: number; - activeReindexJobId: number; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 3866d1a7199e4..38a6187d290b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -4,6 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; + +export interface Schema { + [key: string]: SchemaTypes; +} + +// this is a mapping of schema field types ("string", "number", "geolocation", "date") to the names +// of source engines which utilize that type +export type SchemaConflictFieldTypes = { + [key in SchemaTypes]: string[]; +}; + +export interface SchemaConflict { + fieldTypes: SchemaConflictFieldTypes; + resolution?: string; +} + +// For now these values are ISchemaConflictFieldTypes, but in the near future will be ISchemaConflict +// once we implement schema conflict resolution +export interface SchemaConflicts { + [key: string]: SchemaConflictFieldTypes; +} + export interface IIndexingStatus { percentageComplete: number; numDocumentsWithErrors: number; From d47c70cd53ffa818514be5acbb1f31ce207ba2f7 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:14:19 -0500 Subject: [PATCH 074/107] [Security Solution][Exceptions] Implement exceptions for ML rules (#84006) * Implement exceptions for ML rules * Remove unused import * Better implicit types * Retrieve ML rule index pattern for exception field suggestions and autocomplete * Add ML job logic to edit exception modal * Remove unnecessary logic change Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../detection_engine/get_query_filter.test.ts | 72 +++++--- .../detection_engine/get_query_filter.ts | 79 +++++---- .../exceptions/add_exception_modal/index.tsx | 23 ++- .../exceptions/edit_exception_modal/index.tsx | 149 +++++++++------- .../common/components/ml/api/get_jobs.ts | 35 ++++ .../components/ml/hooks/use_get_jobs.ts | 59 +++++++ .../timeline_actions/alert_context_menu.tsx | 19 +- .../rules/step_about_rule/index.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 3 +- .../signals/__mocks__/es_results.ts | 8 +- .../signals/bulk_create_ml_signals.ts | 2 +- .../signals/filter_events_with_list.test.ts | 162 ++++++++++++++++++ .../signals/filter_events_with_list.ts | 71 +++++--- .../signals/find_ml_signals.ts | 4 + .../signals/signal_rule_alert_type.ts | 21 ++- .../server/lib/machine_learning/index.test.ts | 2 + .../server/lib/machine_learning/index.ts | 16 +- 17 files changed, 552 insertions(+), 179 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts index 8ff75b25388b0..4fff99b09d4ad 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, buildExceptionFilter, buildEqlSearchRequest } from './get_query_filter'; import { Filter, EsQueryConfig } from 'src/plugins/data/public'; import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../shared_imports'; describe('get_filter', () => { describe('getQueryFilter', () => { @@ -919,19 +920,27 @@ describe('get_filter', () => { dateFormatTZ: 'Zulu', }; test('it should build a filter without chunking exception items', () => { - const exceptionFilter = buildExceptionFilter( - [ - { language: 'kuery', query: 'host.name: linux and some.field: value' }, - { language: 'kuery', query: 'user.name: name' }, + const exceptionItem1: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'host.name', operator: 'included', type: 'match', value: 'linux' }, + { field: 'some.field', operator: 'included', type: 'match', value: 'value' }, ], - { + }; + const exceptionItem2: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }], + }; + const exceptionFilter = buildExceptionFilter({ + lists: [exceptionItem1, exceptionItem2], + config, + excludeExceptions: true, + chunkSize: 2, + indexPattern: { fields: [], title: 'auditbeat-*', }, - config, - true, - 2 - ); + }); expect(exceptionFilter).toEqual({ meta: { alias: null, @@ -949,7 +958,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'host.name': 'linux', }, }, @@ -961,7 +970,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'some.field': 'value', }, }, @@ -976,7 +985,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'user.name': 'name', }, }, @@ -990,20 +999,31 @@ describe('get_filter', () => { }); test('it should properly chunk exception items', () => { - const exceptionFilter = buildExceptionFilter( - [ - { language: 'kuery', query: 'host.name: linux and some.field: value' }, - { language: 'kuery', query: 'user.name: name' }, - { language: 'kuery', query: 'file.path: /safe/path' }, + const exceptionItem1: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'host.name', operator: 'included', type: 'match', value: 'linux' }, + { field: 'some.field', operator: 'included', type: 'match', value: 'value' }, ], - { + }; + const exceptionItem2: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }], + }; + const exceptionItem3: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'file.path', operator: 'included', type: 'match', value: '/safe/path' }], + }; + const exceptionFilter = buildExceptionFilter({ + lists: [exceptionItem1, exceptionItem2, exceptionItem3], + config, + excludeExceptions: true, + chunkSize: 2, + indexPattern: { fields: [], title: 'auditbeat-*', }, - config, - true, - 2 - ); + }); expect(exceptionFilter).toEqual({ meta: { alias: null, @@ -1024,7 +1044,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'host.name': 'linux', }, }, @@ -1036,7 +1056,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'some.field': 'value', }, }, @@ -1051,7 +1071,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'user.name': 'name', }, }, @@ -1069,7 +1089,7 @@ describe('get_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { 'file.path': '/safe/path', }, }, diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 73638fc48f381..fcea90402d89d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -6,7 +6,6 @@ import { Filter, - Query, IIndexPattern, isFilterDisabled, buildEsQuery, @@ -18,15 +17,10 @@ import { } from '../../../lists/common/schemas'; import { ESBoolQuery } from '../typed_json'; import { buildExceptionListQueries } from './build_exceptions_query'; -import { - Query as QueryString, - Language, - Index, - TimestampOverrideOrUndefined, -} from './schemas/common/schemas'; +import { Query, Language, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; export const getQueryFilter = ( - query: QueryString, + query: Query, language: Language, filters: Array<Partial<Filter>>, index: Index, @@ -53,19 +47,18 @@ export const getQueryFilter = ( * buildEsQuery, this allows us to offer nested queries * regardless */ - const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists }); - if (exceptionQueries.length > 0) { - // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), - // allowing us to make 1024-item chunks of exception list items. - // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a - // very conservative value. - const exceptionFilter = buildExceptionFilter( - exceptionQueries, - indexPattern, - config, - excludeExceptions, - 1024 - ); + // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), + // allowing us to make 1024-item chunks of exception list items. + // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a + // very conservative value. + const exceptionFilter = buildExceptionFilter({ + lists, + config, + excludeExceptions, + chunkSize: 1024, + indexPattern, + }); + if (exceptionFilter !== undefined) { enabledFilters.push(exceptionFilter); } const initialQuery = { query, language }; @@ -101,15 +94,17 @@ export const buildEqlSearchRequest = ( ignoreFilterIfFieldNotInIndex: false, dateFormatTZ: 'Zulu', }; - const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists: exceptionLists }); - let exceptionFilter: Filter | undefined; - if (exceptionQueries.length > 0) { - // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), - // allowing us to make 1024-item chunks of exception list items. - // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a - // very conservative value. - exceptionFilter = buildExceptionFilter(exceptionQueries, indexPattern, config, true, 1024); - } + // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value), + // allowing us to make 1024-item chunks of exception list items. + // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a + // very conservative value. + const exceptionFilter = buildExceptionFilter({ + lists: exceptionLists, + config, + excludeExceptions: true, + chunkSize: 1024, + indexPattern, + }); const indexString = index.join(); const requestFilter: unknown[] = [ { @@ -154,13 +149,23 @@ export const buildEqlSearchRequest = ( } }; -export const buildExceptionFilter = ( - exceptionQueries: Query[], - indexPattern: IIndexPattern, - config: EsQueryConfig, - excludeExceptions: boolean, - chunkSize: number -) => { +export const buildExceptionFilter = ({ + lists, + config, + excludeExceptions, + chunkSize, + indexPattern, +}: { + lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>; + config: EsQueryConfig; + excludeExceptions: boolean; + chunkSize: number; + indexPattern?: IIndexPattern; +}) => { + const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists }); + if (exceptionQueries.length === 0) { + return undefined; + } const exceptionFilter: Filter = { meta: { alias: null, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index b2f8426413b12..0bbe4c71ef5a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint complexity: ["error", 30]*/ + import React, { memo, useEffect, useState, useCallback, useMemo } from 'react'; import styled, { css } from 'styled-components'; import { @@ -53,6 +55,7 @@ import { import { ErrorInfo, ErrorCallout } from '../error_callout'; import { ExceptionsBuilderExceptionItem } from '../types'; import { useFetchIndex } from '../../../containers/source'; +import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs'; export interface AddExceptionModalProps { ruleName: string; @@ -108,7 +111,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const { http } = useKibana().services; const [errorsExist, setErrorExists] = useState(false); const [comment, setComment] = useState(''); - const { rule: maybeRule } = useRuleAsync(ruleId); + const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId); const [shouldCloseAlert, setShouldCloseAlert] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false); @@ -124,8 +127,22 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex( memoSignalIndexName ); - const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices); + const memoMlJobIds = useMemo( + () => (maybeRule?.machine_learning_job_id != null ? [maybeRule.machine_learning_job_id] : []), + [maybeRule] + ); + const { loading: mlJobLoading, jobs } = useGetInstalledJob(memoMlJobIds); + + const memoRuleIndices = useMemo(() => { + if (jobs.length > 0) { + return jobs[0].results_index_name ? [`.ml-anomalies-${jobs[0].results_index_name}`] : []; + } else { + return ruleIndices; + } + }, [jobs, ruleIndices]); + + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(memoRuleIndices); const onError = useCallback( (error: Error): void => { addError(error, { title: i18n.ADD_EXCEPTION_ERROR }); @@ -364,6 +381,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ !isSignalIndexPatternLoading && !isLoadingExceptionList && !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && ruleExceptionList && ( <> <ModalBodySection className="builder-section"> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index ab0c566aa55c6..e97f745d6f979 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -47,6 +47,7 @@ import { } from '../helpers'; import { Loader } from '../../loader'; import { ErrorInfo, ErrorCallout } from '../error_callout'; +import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs'; interface EditExceptionModalProps { ruleName: string; @@ -100,7 +101,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const { http } = useKibana().services; const [comment, setComment] = useState(''); const [errorsExist, setErrorExists] = useState(false); - const { rule: maybeRule } = useRuleAsync(ruleId); + const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId); const [updateError, setUpdateError] = useState<ErrorInfo | null>(null); const [hasVersionConflict, setHasVersionConflict] = useState(false); const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false); @@ -117,7 +118,21 @@ export const EditExceptionModal = memo(function EditExceptionModal({ memoSignalIndexName ); - const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices); + const memoMlJobIds = useMemo( + () => (maybeRule?.machine_learning_job_id != null ? [maybeRule.machine_learning_job_id] : []), + [maybeRule] + ); + const { loading: mlJobLoading, jobs } = useGetInstalledJob(memoMlJobIds); + + const memoRuleIndices = useMemo(() => { + if (jobs.length > 0) { + return jobs[0].results_index_name ? [`.ml-anomalies-${jobs[0].results_index_name}`] : []; + } else { + return ruleIndices; + } + }, [jobs, ruleIndices]); + + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(memoRuleIndices); const handleExceptionUpdateError = useCallback( (error: Error, statusCode: number | null, message: string | null) => { @@ -280,69 +295,75 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( <Loader data-test-subj="loadingEditExceptionModal" size="xl" /> )} - {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( - <> - <ModalBodySection className="builder-section"> - {isRuleEQLSequenceStatement && ( - <> - <EuiCallOut - data-test-subj="eql-sequence-callout" - title={i18n.EDIT_EXCEPTION_SEQUENCE_WARNING} - /> - <EuiSpacer /> - </> - )} - <EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText> - <EuiSpacer /> - <ExceptionBuilderComponent - exceptionListItems={[exceptionItem]} - listType={exceptionListType} - listId={exceptionItem.list_id} - listNamespaceType={exceptionItem.namespace_type} - ruleName={ruleName} - isOrDisabled - isAndDisabled={false} - isNestedDisabled={false} - data-test-subj="edit-exception-modal-builder" - id-aria="edit-exception-modal-builder" - onChange={handleBuilderOnChange} - indexPatterns={indexPatterns} - ruleType={maybeRule?.type} - /> - - <EuiSpacer /> - - <AddExceptionComments - exceptionItemComments={exceptionItem.comments} - newCommentValue={comment} - newCommentOnChange={onCommentChange} - /> - </ModalBodySection> - <EuiHorizontalRule /> - <ModalBodySection> - <EuiFormRow fullWidth> - <EuiCheckbox - data-test-subj="close-alert-on-add-edit-exception-checkbox" - id="close-alert-on-add-edit-exception-checkbox" - label={ - shouldDisableBulkClose ? i18n.BULK_CLOSE_LABEL_DISABLED : i18n.BULK_CLOSE_LABEL - } - checked={shouldBulkCloseAlert} - onChange={onBulkCloseAlertCheckboxChange} - disabled={shouldDisableBulkClose} + {!isSignalIndexLoading && + !addExceptionIsLoading && + !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && ( + <> + <ModalBodySection className="builder-section"> + {isRuleEQLSequenceStatement && ( + <> + <EuiCallOut + data-test-subj="eql-sequence-callout" + title={i18n.EDIT_EXCEPTION_SEQUENCE_WARNING} + /> + <EuiSpacer /> + </> + )} + <EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText> + <EuiSpacer /> + <ExceptionBuilderComponent + exceptionListItems={[exceptionItem]} + listType={exceptionListType} + listId={exceptionItem.list_id} + listNamespaceType={exceptionItem.namespace_type} + ruleName={ruleName} + isOrDisabled + isAndDisabled={false} + isNestedDisabled={false} + data-test-subj="edit-exception-modal-builder" + id-aria="edit-exception-modal-builder" + onChange={handleBuilderOnChange} + indexPatterns={indexPatterns} + ruleType={maybeRule?.type} /> - </EuiFormRow> - {exceptionListType === 'endpoint' && ( - <> - <EuiSpacer /> - <EuiText data-test-subj="edit-exception-endpoint-text" color="subdued" size="s"> - {i18n.ENDPOINT_QUARANTINE_TEXT} - </EuiText> - </> - )} - </ModalBodySection> - </> - )} + + <EuiSpacer /> + + <AddExceptionComments + exceptionItemComments={exceptionItem.comments} + newCommentValue={comment} + newCommentOnChange={onCommentChange} + /> + </ModalBodySection> + <EuiHorizontalRule /> + <ModalBodySection> + <EuiFormRow fullWidth> + <EuiCheckbox + data-test-subj="close-alert-on-add-edit-exception-checkbox" + id="close-alert-on-add-edit-exception-checkbox" + label={ + shouldDisableBulkClose + ? i18n.BULK_CLOSE_LABEL_DISABLED + : i18n.BULK_CLOSE_LABEL + } + checked={shouldBulkCloseAlert} + onChange={onBulkCloseAlertCheckboxChange} + disabled={shouldDisableBulkClose} + /> + </EuiFormRow> + {exceptionListType === 'endpoint' && ( + <> + <EuiSpacer /> + <EuiText data-test-subj="edit-exception-endpoint-text" color="subdued" size="s"> + {i18n.ENDPOINT_QUARANTINE_TEXT} + </EuiText> + </> + )} + </ModalBodySection> + </> + )} {updateError != null && ( <ModalBodySection> <ErrorCallout diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs.ts new file mode 100644 index 0000000000000..52d2c3bb32129 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CombinedJobWithStats } from '../../../../../../ml/common/types/anomaly_detection_jobs'; +import { HttpSetup } from '../../../../../../../../src/core/public'; + +export interface GetJobsArgs { + http: HttpSetup; + jobIds: string[]; + signal: AbortSignal; +} + +/** + * Fetches details for a set of ML jobs + * + * @param http HTTP Service + * @param jobIds Array of job IDs to filter against + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const getJobs = async ({ + http, + jobIds, + signal, +}: GetJobsArgs): Promise<CombinedJobWithStats[]> => + http.fetch<CombinedJobWithStats[]>('/api/ml/jobs/jobs', { + method: 'POST', + body: JSON.stringify({ jobIds }), + asSystemRequest: true, + signal, + }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts new file mode 100644 index 0000000000000..4d7b342773d6c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; +import { getJobs } from '../api/get_jobs'; +import { CombinedJobWithStats } from '../../../../../../ml/common/types/anomaly_detection_jobs'; + +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useMlCapabilities } from './use_ml_capabilities'; +import * as i18n from '../translations'; + +const _getJobs = withOptionalSignal(getJobs); + +export const useGetJobs = () => useAsync(_getJobs); + +export interface UseGetInstalledJobReturn { + loading: boolean; + jobs: CombinedJobWithStats[]; + isMlUser: boolean; + isLicensed: boolean; +} + +export const useGetInstalledJob = (jobIds: string[]): UseGetInstalledJobReturn => { + const [jobs, setJobs] = useState<CombinedJobWithStats[]>([]); + const { addError } = useAppToasts(); + const mlCapabilities = useMlCapabilities(); + const http = useHttp(); + const { error, loading, result, start } = useGetJobs(); + + const isMlUser = hasMlUserPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + if (isMlUser && isLicensed && jobIds.length > 0) { + start({ http, jobIds }); + } + }, [http, isMlUser, isLicensed, start, jobIds]); + + useEffect(() => { + if (result) { + setJobs(result); + } + }, [result]); + + useEffect(() => { + if (error) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + }, [addError, error]); + + return { isLicensed, isMlUser, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index cf8204478a955..9eb0a97a1c9a2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -21,7 +21,6 @@ import { TimelineId } from '../../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; import { Status, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { timelineActions } from '../../../../timelines/store/timeline'; import { EventsTd, EventsTdContent } from '../../../../timelines/components/timeline/styles'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; @@ -75,11 +74,17 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({ '', [ecsRowData] ); - const ruleIndices = useMemo( - (): string[] => - (ecsRowData.signal?.rule && ecsRowData.signal.rule.index) ?? DEFAULT_INDEX_PATTERN, - [ecsRowData] - ); + const ruleIndices = useMemo((): string[] => { + if ( + ecsRowData.signal?.rule && + ecsRowData.signal.rule.index && + ecsRowData.signal.rule.index.length > 0 + ) { + return ecsRowData.signal.rule.index; + } else { + return DEFAULT_INDEX_PATTERN; + } + }, [ecsRowData]); const { addWarning } = useAppToasts(); @@ -321,7 +326,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({ const areExceptionsAllowed = useMemo((): boolean => { const ruleTypes = getOr([], 'signal.rule.type', ecsRowData); const [ruleType] = ruleTypes as Type[]; - return !isMlRule(ruleType) && !isThresholdRule(ruleType); + return !isThresholdRule(ruleType); }, [ecsRowData]); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 2479a260872be..40b73fc7d158c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -8,7 +8,6 @@ import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleStepProps, @@ -76,10 +75,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({ const [severityValue, setSeverityValue] = useState<string>(initialState.severity.value); const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []); - const canUseExceptions = - defineRuleData?.ruleType && - !isMlRule(defineRuleData.ruleType) && - !isThresholdRule(defineRuleData.ruleType); + const canUseExceptions = defineRuleData?.ruleType && !isThresholdRule(defineRuleData.ruleType); const { form } = useForm<AboutStepRule>({ defaultValue: initialState, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4866824f882cf..d04980d764831 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -80,7 +80,6 @@ import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; -import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers'; @@ -104,7 +103,7 @@ enum RuleDetailTabs { } const getRuleDetailsTabs = (rule: Rule | null) => { - const canUseExceptions = rule && !isMlRule(rule.type) && !isThresholdRule(rule.type); + const canUseExceptions = rule && !isThresholdRule(rule.type); return [ { id: RuleDetailTabs.alerts, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 0a38bdc790b41..764604a793788 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -150,8 +150,8 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): Sig export const sampleDocWithSortId = ( someUuid: string = sampleIdGuid, - ip?: string, - destIp?: string + ip?: string | string[], + destIp?: string | string[] ): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', @@ -502,8 +502,8 @@ export const repeatedSearchResultsWithSortId = ( total: number, pageSize: number, guids: string[], - ips?: string[], - destIps?: string[] + ips?: Array<string | string[]>, + destIps?: Array<string | string[]> ): SignalSearchResponse => ({ took: 10, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 5c2dfa62e5951..d530fe10c6498 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -6,7 +6,6 @@ import { flow, omit } from 'lodash/fp'; import set from 'set-value'; -import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; import { AlertServices } from '../../../../../alerts/server'; @@ -15,6 +14,7 @@ import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; +import { SearchResponse } from '../../types'; interface BulkCreateMlSignalsParams { actions: RuleAlertAction[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 3334cc17b9050..01e7e7160e1ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -400,6 +400,87 @@ describe('filterEventsAgainstList', () => { '9.9.9.9', ]).toEqual(ipVals); }); + + it('should respond with less items in the list given one exception item with two entries of type list and array of values in document', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys-again.txt', + type: 'ip', + }, + }, + ]; + + // this call represents an exception list with a value list containing ['2.2.2.2'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '2.2.2.2' }, + ]); + // this call represents an exception list with a value list containing ['4.4.4.4'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '4.4.4.4' }, + ]); + + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient, + exceptionsList: [exceptionItem], + eventSearchResult: repeatedSearchResultsWithSortId( + 3, + 3, + someGuids.slice(0, 3), + [ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ], + [ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], + ] + ), + buildRuleMessage, + }); + expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + ]); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '4.4.4.4', + ]); + expect(res.hits.hits.length).toEqual(2); + + // @ts-expect-error + const sourceIpVals = res.hits.hits.map((item) => item._source.source.ip); + expect([ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ]).toEqual(sourceIpVals); + // @ts-expect-error + const destIpVals = res.hits.hits.map((item) => item._source.destination.ip); + expect([ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ]).toEqual(destIpVals); + }); }); describe('operator type is excluded', () => { it('should respond with empty list if no items match value list', async () => { @@ -463,5 +544,86 @@ describe('filterEventsAgainstList', () => { ); expect(res.hits.hits.length).toEqual(2); }); + + it('should respond with less items in the list given one exception item with two entries of type list and array of values in document', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys-again.txt', + type: 'ip', + }, + }, + ]; + + // this call represents an exception list with a value list containing ['2.2.2.2'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '2.2.2.2' }, + ]); + // this call represents an exception list with a value list containing ['4.4.4.4'] + (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getListItemResponseMock(), value: '4.4.4.4' }, + ]); + + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient, + exceptionsList: [exceptionItem], + eventSearchResult: repeatedSearchResultsWithSortId( + 3, + 3, + someGuids.slice(0, 3), + [ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ], + [ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], + ] + ), + buildRuleMessage, + }); + expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + ]); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '4.4.4.4', + ]); + expect(res.hits.hits.length).toEqual(2); + + // @ts-expect-error + const sourceIpVals = res.hits.hits.map((item) => item._source.source.ip); + expect([ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ]).toEqual(sourceIpVals); + // @ts-expect-error + const destIpVals = res.hits.hits.map((item) => item._source.destination.ip); + expect([ + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], + ]).toEqual(destIpVals); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 908d073fbeabd..1c13de16d9b1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -7,7 +7,6 @@ import { get } from 'lodash/fp'; import { Logger } from 'src/core/server'; import { ListClient } from '../../../../../lists/server'; -import { SignalSearchResponse } from './types'; import { BuildRuleMessage } from './rule_messages'; import { EntryList, @@ -17,16 +16,23 @@ import { } from '../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../common/detection_engine/utils'; import { SearchTypes } from '../../../../common/detection_engine/types'; +import { SearchResponse } from '../../types'; -interface FilterEventsAgainstList { - listClient: ListClient; - exceptionsList: ExceptionListItemSchema[]; - logger: Logger; - eventSearchResult: SignalSearchResponse; - buildRuleMessage: BuildRuleMessage; -} +// narrow unioned type to be single +const isStringableType = (val: SearchTypes): val is string | number | boolean => + ['string', 'number', 'boolean'].includes(typeof val); + +const isStringableArray = (val: SearchTypes): val is Array<string | number | boolean> => { + if (!Array.isArray(val)) { + return false; + } + // TS does not allow .every to be called on val as-is, even though every type in the union + // is an array. https://github.com/microsoft/TypeScript/issues/36390 + // @ts-expect-error + return val.every((subVal) => isStringableType(subVal)); +}; -export const createSetToFilterAgainst = async ({ +export const createSetToFilterAgainst = async <T>({ events, field, listId, @@ -35,7 +41,7 @@ export const createSetToFilterAgainst = async ({ logger, buildRuleMessage, }: { - events: SignalSearchResponse['hits']['hits']; + events: SearchResponse<T>['hits']['hits']; field: string; listId: string; listType: Type; @@ -43,13 +49,14 @@ export const createSetToFilterAgainst = async ({ logger: Logger; buildRuleMessage: BuildRuleMessage; }): Promise<Set<SearchTypes>> => { - // narrow unioned type to be single - const isStringableType = (val: SearchTypes) => - ['string', 'number', 'boolean'].includes(typeof val); const valuesFromSearchResultField = events.reduce((acc, searchResultItem) => { const valueField = get(field, searchResultItem._source); - if (valueField != null && isStringableType(valueField)) { - acc.add(valueField.toString()); + if (valueField != null) { + if (isStringableType(valueField)) { + acc.add(valueField.toString()); + } else if (isStringableArray(valueField)) { + valueField.forEach((subVal) => acc.add(subVal.toString())); + } } return acc; }, new Set<string>()); @@ -71,13 +78,19 @@ export const createSetToFilterAgainst = async ({ return matchedListItemsSet; }; -export const filterEventsAgainstList = async ({ +export const filterEventsAgainstList = async <T>({ listClient, exceptionsList, logger, eventSearchResult, buildRuleMessage, -}: FilterEventsAgainstList): Promise<SignalSearchResponse> => { +}: { + listClient: ListClient; + exceptionsList: ExceptionListItemSchema[]; + logger: Logger; + eventSearchResult: SearchResponse<T>; + buildRuleMessage: BuildRuleMessage; +}): Promise<SearchResponse<T>> => { try { if (exceptionsList == null || exceptionsList.length === 0) { logger.debug(buildRuleMessage('about to return original search result')); @@ -108,9 +121,9 @@ export const filterEventsAgainstList = async ({ }); // now that we have all the exception items which are value lists (whether single entry or have multiple entries) - const res = await valueListExceptionItems.reduce<Promise<SignalSearchResponse['hits']['hits']>>( + const res = await valueListExceptionItems.reduce<Promise<SearchResponse<T>['hits']['hits']>>( async ( - filteredAccum: Promise<SignalSearchResponse['hits']['hits']>, + filteredAccum: Promise<SearchResponse<T>['hits']['hits']>, exceptionItem: ExceptionListItemSchema ) => { // 1. acquire the values from the specified fields to check @@ -152,15 +165,23 @@ export const filterEventsAgainstList = async ({ const vals = fieldAndSetTuples.map((tuple) => { const eventItem = get(tuple.field, item._source); if (tuple.operator === 'included') { - // only create a signal if the event is not in the value list + // only create a signal if the field value is not in the value list if (eventItem != null) { - return !tuple.matchedSet.has(eventItem); + if (isStringableType(eventItem)) { + return !tuple.matchedSet.has(eventItem); + } else if (isStringableArray(eventItem)) { + return !eventItem.some((val) => tuple.matchedSet.has(val)); + } } return true; } else if (tuple.operator === 'excluded') { - // only create a signal if the event is in the value list + // only create a signal if the field value is in the value list if (eventItem != null) { - return tuple.matchedSet.has(eventItem); + if (isStringableType(eventItem)) { + return tuple.matchedSet.has(eventItem); + } else if (isStringableArray(eventItem)) { + return eventItem.some((val) => tuple.matchedSet.has(val)); + } } return true; } @@ -175,10 +196,10 @@ export const filterEventsAgainstList = async ({ const toReturn = filteredEvents; return toReturn; }, - Promise.resolve<SignalSearchResponse['hits']['hits']>(eventSearchResult.hits.hits) + Promise.resolve<SearchResponse<T>['hits']['hits']>(eventSearchResult.hits.hits) ); - const toReturn: SignalSearchResponse = { + const toReturn: SearchResponse<T> = { took: eventSearchResult.took, timed_out: eventSearchResult.timed_out, _shards: eventSearchResult._shards, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts index 94b73fce79f0c..ec653f088b523 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts @@ -5,6 +5,7 @@ */ import dateMath from '@elastic/datemath'; +import { ExceptionListItemSchema } from '../../../../../lists/common'; import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../../ml/server'; @@ -18,6 +19,7 @@ export const findMlSignals = async ({ anomalyThreshold, from, to, + exceptionItems, }: { ml: MlPluginSetup; request: KibanaRequest; @@ -26,6 +28,7 @@ export const findMlSignals = async ({ anomalyThreshold: number; from: string; to: string; + exceptionItems: ExceptionListItemSchema[]; }): Promise<AnomalyResults> => { const { mlAnomalySearch } = ml.mlSystemProvider(request, savedObjectsClient); const params = { @@ -33,6 +36,7 @@ export const findMlSignals = async ({ threshold: anomalyThreshold, earliestMs: dateMath.parse(from)?.valueOf() ?? 0, latestMs: dateMath.parse(to)?.valueOf() ?? 0, + exceptionItems, }; return getAnomalies(params, mlAnomalySearch); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index f0b1825c7cc99..d6bdc14a92b40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -66,6 +66,7 @@ import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk import { createThreatSignals } from './threat_mapping/create_threat_signals'; import { getIndexVersion } from '../routes/index/get_index_version'; import { MIN_EQL_RULE_INDEX_VERSION } from '../routes/index/get_signals_template'; +import { filterEventsAgainstList } from './filter_events_with_list'; export const signalRulesAlertType = ({ logger, @@ -242,9 +243,18 @@ export const signalRulesAlertType = ({ anomalyThreshold, from, to, + exceptionItems: exceptionItems ?? [], + }); + + const filteredAnomalyResults = await filterEventsAgainstList({ + listClient, + exceptionsList: exceptionItems ?? [], + logger, + eventSearchResult: anomalyResults, + buildRuleMessage, }); - const anomalyCount = anomalyResults.hits.hits.length; + const anomalyCount = filteredAnomalyResults.hits.hits.length; if (anomalyCount) { logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); } @@ -257,7 +267,7 @@ export const signalRulesAlertType = ({ } = await bulkCreateMlSignals({ actions, throttle, - someResult: anomalyResults, + someResult: filteredAnomalyResults, ruleParams: params, services, logger, @@ -276,15 +286,16 @@ export const signalRulesAlertType = ({ }); // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } const shardFailures = - (anomalyResults._shards as typeof anomalyResults._shards & { failures: [] }).failures ?? - []; + (filteredAnomalyResults._shards as typeof filteredAnomalyResults._shards & { + failures: []; + }).failures ?? []; const searchErrors = createErrorsFromShard({ errors: shardFailures, }); result = mergeReturns([ result, createSearchAfterReturnType({ - success: success && anomalyResults._shards.failed === 0, + success: success && filteredAnomalyResults._shards.failed === 0, errors: [...errors, ...searchErrors], createdSignalsCount: createdItemsCount, bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts index 63e3f3487e482..d08b5e649451c 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getAnomalies, AnomaliesSearchParams } from '.'; const getFiltersFromMock = (mock: jest.Mock) => { @@ -23,6 +24,7 @@ describe('getAnomalies', () => { threshold: 5, earliestMs: 1588517231429, latestMs: 1588617231429, + exceptionItems: [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()], }; }); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index 34e004d817fe7..ec801f6c49ae7 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { RequestParams } from '@elastic/elasticsearch'; +import { ExceptionListItemSchema } from '../../../../lists/common'; +import { buildExceptionFilter } from '../../../common/detection_engine/get_query_filter'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; +import { SearchResponse } from '../types'; export { Anomaly }; export type AnomalyResults = SearchResponse<Anomaly>; @@ -21,6 +23,7 @@ export interface AnomaliesSearchParams { threshold: number; earliestMs: number; latestMs: number; + exceptionItems: ExceptionListItemSchema[]; maxRecords?: number; } @@ -49,6 +52,17 @@ export const getAnomalies = async ( }, }, ], + must_not: buildExceptionFilter({ + lists: params.exceptionItems, + config: { + allowLeadingWildcards: true, + queryStringOptions: { analyze_wildcard: true }, + ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: 'Zulu', + }, + excludeExceptions: true, + chunkSize: 1024, + })?.query, }, }, sort: [{ record_score: { order: 'desc' } }], From e1944342af1a4f9834092e3ef31ab9cf832e5274 Mon Sep 17 00:00:00 2001 From: Dan Panzarella <pzl@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:30:44 -0500 Subject: [PATCH 075/107] [Security Solution] Keep Endpoint policies up to date with license changes (#83992) --- .../endpoint/lib/policy/license_watch.test.ts | 133 ++++++++++++++++++ .../endpoint/lib/policy/license_watch.ts | 116 +++++++++++++++ .../security_solution/server/plugin.ts | 10 +- 3 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts new file mode 100644 index 0000000000000..5773b88fa2bea --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Subject } from 'rxjs'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { LicenseService } from '../../../../common/license/license'; +import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; +import { PolicyWatcher } from './license_watch'; +import { ILicense } from '../../../../../licensing/common/types'; +import { licenseMock } from '../../../../../licensing/common/licensing.mock'; +import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { PackagePolicy } from '../../../../../fleet/common'; +import { createPackagePolicyMock } from '../../../../../fleet/common/mocks'; +import { factory } from '../../../../common/endpoint/models/policy_config'; +import { PolicyConfig } from '../../../../common/endpoint/types'; + +const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): PackagePolicy => { + const packagePolicy = createPackagePolicyMock(); + if (!cb) { + // eslint-disable-next-line no-param-reassign + cb = (p) => p; + } + const policyConfig = cb(factory()); + packagePolicy.inputs[0].config = { policy: { value: policyConfig } }; + return packagePolicy; +}; + +describe('Policy-Changing license watcher', () => { + const logger = loggingSystemMock.create().get('license_watch.test'); + const soStartMock = savedObjectsServiceMock.createStartContract(); + let packagePolicySvcMock: jest.Mocked<PackagePolicyServiceInterface>; + + const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); + const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); + const Basic = licenseMock.createLicense({ license: { type: 'basic', mode: 'basic' } }); + + beforeEach(() => { + packagePolicySvcMock = createPackagePolicyServiceMock(); + }); + + it('is activated on license changes', () => { + // mock a license-changing service to test reactivity + const licenseEmitter: Subject<ILicense> = new Subject(); + const licenseService = new LicenseService(); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + + // swap out watch function, just to ensure it gets called when a license change happens + const mockWatch = jest.fn(); + pw.watch = mockWatch; + + // licenseService is watching our subject for incoming licenses + licenseService.start(licenseEmitter); + pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well + + // Enqueue a license change! + licenseEmitter.next(Platinum); + + // policywatcher should have triggered + expect(mockWatch.mock.calls.length).toBe(1); + + pw.stop(); + licenseService.stop(); + licenseEmitter.complete(); + }); + + it('pages through all endpoint policies', async () => { + const TOTAL = 247; + + // set up the mocked package policy service to return and do what we want + packagePolicySvcMock.list + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, () => MockPPWithEndpointPolicy()), + total: TOTAL, + page: 1, + perPage: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, () => MockPPWithEndpointPolicy()), + total: TOTAL, + page: 2, + perPage: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: TOTAL - 200 }, () => MockPPWithEndpointPolicy()), + total: TOTAL, + page: 3, + perPage: 100, + }); + + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + await pw.watch(Gold); // just manually trigger with a given license + + expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts + + // Assert: on the first call to packagePolicy.list, we asked for page 1 + expect(packagePolicySvcMock.list.mock.calls[0][1].page).toBe(1); + expect(packagePolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2 + expect(packagePolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc + }); + + it('alters no-longer-licensed features', async () => { + const CustomMessage = 'Custom string'; + + // mock a Policy with a higher-tiered feature enabled + packagePolicySvcMock.list.mockResolvedValueOnce({ + items: [ + MockPPWithEndpointPolicy( + (pc: PolicyConfig): PolicyConfig => { + pc.windows.popup.malware.message = CustomMessage; + return pc; + } + ), + ], + total: 1, + page: 1, + perPage: 100, + }); + + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + + // emulate a license change below paid tier + await pw.watch(Basic); + + expect(packagePolicySvcMock.update).toHaveBeenCalled(); + expect( + packagePolicySvcMock.update.mock.calls[0][2].inputs[0].config!.policy.value.windows.popup + .malware.message + ).not.toEqual(CustomMessage); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts new file mode 100644 index 0000000000000..cae3b9f33850a --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -0,0 +1,116 @@ +/* + * 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 { Subscription } from 'rxjs'; + +import { + KibanaRequest, + Logger, + SavedObjectsClientContract, + SavedObjectsServiceStart, +} from 'src/core/server'; +import { PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../fleet/common'; +import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { ILicense } from '../../../../../licensing/common/types'; +import { + isEndpointPolicyValidForLicense, + unsetPolicyFeaturesAboveLicenseLevel, +} from '../../../../common/license/policy_config'; +import { isAtLeast, LicenseService } from '../../../../common/license/license'; + +export class PolicyWatcher { + private logger: Logger; + private soClient: SavedObjectsClientContract; + private policyService: PackagePolicyServiceInterface; + private subscription: Subscription | undefined; + constructor( + policyService: PackagePolicyServiceInterface, + soStart: SavedObjectsServiceStart, + logger: Logger + ) { + this.policyService = policyService; + this.soClient = this.makeInternalSOClient(soStart); + this.logger = logger; + } + + /** + * The policy watcher is not called as part of a HTTP request chain, where the + * request-scoped SOClient could be passed down. It is called via license observable + * changes. We are acting as the 'system' in response to license changes, so we are + * intentionally using the system user here. Be very aware of what you are using this + * client to do + */ + private makeInternalSOClient(soStart: SavedObjectsServiceStart): SavedObjectsClientContract { + const fakeRequest = ({ + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { href: {} }, + raw: { req: { url: '/' } }, + } as unknown) as KibanaRequest; + return soStart.getScopedClient(fakeRequest, { excludedWrappers: ['security'] }); + } + + public start(licenseService: LicenseService) { + this.subscription = licenseService.getLicenseInformation$()?.subscribe(this.watch.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public async watch(license: ILicense) { + if (isAtLeast(license, 'platinum')) { + return; + } + + let page = 1; + let response: { + items: PackagePolicy[]; + total: number; + page: number; + perPage: number; + }; + do { + try { + response = await this.policyService.list(this.soClient, { + page: page++, + perPage: 100, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, + }); + } catch (e) { + this.logger.warn( + `Unable to verify endpoint policies in line with license change: failed to fetch package policies: ${e.message}` + ); + return; + } + response.items.forEach(async (policy) => { + const policyConfig = policy.inputs[0].config?.policy.value; + if (!isEndpointPolicyValidForLicense(policyConfig, license)) { + policy.inputs[0].config!.policy.value = unsetPolicyFeaturesAboveLicenseLevel( + policyConfig, + license + ); + try { + await this.policyService.update(this.soClient, policy.id, policy); + } catch (e) { + // try again for transient issues + try { + await this.policyService.update(this.soClient, policy.id, policy); + } catch (ee) { + this.logger.warn( + `Unable to remove platinum features from policy ${policy.id}: ${ee.message}` + ); + } + } + } + }); + } while (response.page * response.perPage < response.total); + } +} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 088af40a84ae0..10e817bea0282 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -75,6 +75,7 @@ import { TelemetryPluginSetup, } from '../../../../src/plugins/telemetry/server'; import { licenseService } from './lib/license/license'; +import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; export interface SetupPlugins { alerts: AlertingSetup; @@ -127,6 +128,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S private lists: ListPluginSetup | undefined; // TODO: can we create ListPluginStart? private licensing$!: Observable<ILicense>; + private policyWatcher?: PolicyWatcher; private manifestTask: ManifestTask | undefined; private exceptionsCache: LRU<string, Buffer>; @@ -370,7 +372,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S this.telemetryEventsSender.start(core, plugins.telemetry); this.licensing$ = plugins.licensing.license$; licenseService.start(this.licensing$); - + this.policyWatcher = new PolicyWatcher( + plugins.fleet!.packagePolicyService, + core.savedObjects, + this.logger + ); + this.policyWatcher.start(licenseService); return {}; } @@ -378,6 +385,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S this.logger.debug('Stopping plugin'); this.telemetryEventsSender.stop(); this.endpointAppContextService.stop(); + this.policyWatcher?.stop(); licenseService.stop(); } } From fb48e903d5506f0a9dd235fb494c463eef8e2d70 Mon Sep 17 00:00:00 2001 From: Thomas Watson <w@tson.dk> Date: Wed, 2 Dec 2020 23:40:06 +0100 Subject: [PATCH 076/107] Upgrade Node.js to version 14 (#83425) --- .ci/Dockerfile | 2 +- .node-version | 2 +- .nvmrc | 2 +- package.json | 12 +-- .../kbn-legacy-logging/src/setup_logging.ts | 2 +- packages/kbn-pm/.babelrc | 3 +- .../fake_mocha_types.d.ts | 2 +- .../src/streams/reduce_stream.test.ts | 4 +- src/cli/cluster/cluster.mock.ts | 2 +- src/cli/repl/__snapshots__/repl.test.js.snap | 19 +++-- src/core/public/utils/crypto/sha256.ts | 2 +- .../client/configure_client.test.ts | 2 +- .../server/metrics/collectors/process.test.ts | 1 + .../build/tasks/patch_native_modules_task.ts | 18 ++--- .../public/state_management/url/format.ts | 6 ++ .../state_management/url/kbn_url_storage.ts | 6 ++ .../authentication/login/login_page.tsx | 1 + .../public/management/common/routing.ts | 5 +- .../pages/endpoint_hosts/store/selectors.ts | 2 +- .../lib/timeline/routes/utils/common.ts | 2 +- .../dashboard/reporting/lib/compare_pngs.ts | 17 ++-- yarn.lock | 79 ++++++++++++------- 22 files changed, 116 insertions(+), 75 deletions(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index b2254c8fb1e05..ec7befe05f0d4 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=12.19.1 +ARG NODE_VERSION=14.15.1 FROM node:${NODE_VERSION} AS base diff --git a/.node-version b/.node-version index e9f788b12771f..2f5ee741e0d77 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -12.19.1 +14.15.1 diff --git a/.nvmrc b/.nvmrc index e9f788b12771f..2f5ee741e0d77 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.19.1 +14.15.1 diff --git a/package.json b/package.json index d5c1f247d87d7..77368f5caa7ee 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "**/@types/hapi__boom": "^7.4.1", "**/@types/hapi__hapi": "^18.2.6", "**/@types/hapi__mimos": "4.1.0", - "**/@types/node": "12.19.4", + "**/@types/node": "14.14.7", "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", @@ -98,7 +98,7 @@ "**/typescript": "4.1.2" }, "engines": { - "node": "12.19.1", + "node": "14.15.1", "yarn": "^1.21.1" }, "dependencies": { @@ -109,7 +109,7 @@ "@elastic/ems-client": "7.11.0", "@elastic/eui": "30.2.0", "@elastic/filesaver": "1.1.2", - "@elastic/good": "8.1.1-kibana2", + "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", @@ -496,7 +496,7 @@ "@types/mustache": "^0.8.31", "@types/ncp": "^2.0.1", "@types/nock": "^10.0.3", - "@types/node": "12.19.4", + "@types/node": "14.14.7", "@types/node-fetch": "^2.5.7", "@types/node-forge": "^0.9.5", "@types/nodemailer": "^6.4.0", @@ -722,7 +722,7 @@ "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^0.8.15", + "lmdb-store": "^0.9.0", "load-grunt-config": "^3.0.1", "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", @@ -805,7 +805,7 @@ "sass-resources-loader": "^2.0.1", "selenium-webdriver": "^4.0.0-alpha.7", "serve-static": "1.14.1", - "shelljs": "^0.8.3", + "shelljs": "^0.8.4", "simple-git": "1.116.0", "sinon": "^7.4.2", "spawn-sync": "^1.0.15", diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts index 3b8b4b167c63d..153e7a0f207c1 100644 --- a/packages/kbn-legacy-logging/src/setup_logging.ts +++ b/packages/kbn-legacy-logging/src/setup_logging.ts @@ -18,7 +18,7 @@ */ // @ts-expect-error missing typedef -import good from '@elastic/good'; +import { plugin as good } from '@elastic/good'; import { Server } from '@hapi/hapi'; import { LegacyLoggingConfig } from './schema'; import { getLoggingConfiguration } from './get_logging_config'; diff --git a/packages/kbn-pm/.babelrc b/packages/kbn-pm/.babelrc index 1ca768097a7ee..9ea6ecafe7287 100644 --- a/packages/kbn-pm/.babelrc +++ b/packages/kbn-pm/.babelrc @@ -9,6 +9,7 @@ ], "plugins": [ "@babel/proposal-class-properties", - "@babel/proposal-object-rest-spread" + "@babel/proposal-object-rest-spread", + "@babel/proposal-optional-chaining" ] } diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts index 35b4b85e4d22a..a1e5b2a363a9d 100644 --- a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts +++ b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts @@ -23,7 +23,7 @@ * tries to mock out simple versions of the Mocha types */ -import EventEmitter from 'events'; +import { EventEmitter } from 'events'; export interface Suite { suites: Suite[]; diff --git a/packages/kbn-utils/src/streams/reduce_stream.test.ts b/packages/kbn-utils/src/streams/reduce_stream.test.ts index e4a7dc1cef491..7d823bb8fe113 100644 --- a/packages/kbn-utils/src/streams/reduce_stream.test.ts +++ b/packages/kbn-utils/src/streams/reduce_stream.test.ts @@ -70,7 +70,7 @@ describe('reduceStream', () => { const errorStub = jest.fn(); reduce$.on('data', dataStub); reduce$.on('error', errorStub); - const endEvent = promiseFromEvent('end', reduce$); + const closeEvent = promiseFromEvent('close', reduce$); reduce$.write(1); reduce$.write(2); @@ -79,7 +79,7 @@ describe('reduceStream', () => { reduce$.write(1000); reduce$.end(); - await endEvent; + await closeEvent; expect(reducer).toHaveBeenCalledTimes(3); expect(dataStub).toHaveBeenCalledTimes(0); expect(errorStub).toHaveBeenCalledTimes(1); diff --git a/src/cli/cluster/cluster.mock.ts b/src/cli/cluster/cluster.mock.ts index 332f8aad53ba1..85d16a79a467c 100644 --- a/src/cli/cluster/cluster.mock.ts +++ b/src/cli/cluster/cluster.mock.ts @@ -19,7 +19,7 @@ /* eslint-env jest */ // eslint-disable-next-line max-classes-per-file -import EventEmitter from 'events'; +import { EventEmitter } from 'events'; import { assign, random } from 'lodash'; import { delay } from 'bluebird'; diff --git a/src/cli/repl/__snapshots__/repl.test.js.snap b/src/cli/repl/__snapshots__/repl.test.js.snap index c7751b5797f49..804898284491d 100644 --- a/src/cli/repl/__snapshots__/repl.test.js.snap +++ b/src/cli/repl/__snapshots__/repl.test.js.snap @@ -1,17 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`repl it allows print depth to be specified 1`] = `"{ '0': { '1': { '2': [Object] } }, whoops: [Circular] }"`; +exports[`repl it allows print depth to be specified 1`] = ` +"<ref *1> { + '0': { '1': { '2': [Object] } }, + whoops: [Circular *1] +}" +`; exports[`repl it colorizes raw values 1`] = `"{ meaning: 42 }"`; exports[`repl it handles deep and recursive objects 1`] = ` -"{ +"<ref *1> { '0': { '1': { '2': { '3': { '4': { '5': [Object] } } } } }, - whoops: [Circular] + whoops: [Circular *1] }" `; @@ -51,13 +56,13 @@ Array [ Array [ "Promise Rejected: ", - "{ + "<ref *1> { '0': { '1': { '2': { '3': { '4': { '5': [Object] } } } } }, - whoops: [Circular] + whoops: [Circular *1] }", ], ] @@ -71,13 +76,13 @@ Array [ Array [ "Promise Resolved: ", - "{ + "<ref *1> { '0': { '1': { '2': { '3': { '4': { '5': [Object] } } } } }, - whoops: [Circular] + whoops: [Circular *1] }", ], ] diff --git a/src/core/public/utils/crypto/sha256.ts b/src/core/public/utils/crypto/sha256.ts index 13e0d405a706b..add93cb75b92a 100644 --- a/src/core/public/utils/crypto/sha256.ts +++ b/src/core/public/utils/crypto/sha256.ts @@ -200,7 +200,7 @@ export class Sha256 { return this; } - digest(encoding: string): string { + digest(encoding: BufferEncoding): string { // Suppose the length of the message M, in bits, is l const l = this._len * 8; diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index 614ec112e8f0b..22cb7275b6a23 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -24,7 +24,7 @@ import { TransportRequestParams, RequestBody } from '@elastic/elasticsearch/lib/ import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import EventEmitter from 'events'; +import { EventEmitter } from 'events'; import type { ElasticsearchClientConfig } from './client_config'; import { configureClient } from './configure_client'; diff --git a/src/core/server/metrics/collectors/process.test.ts b/src/core/server/metrics/collectors/process.test.ts index a437d799371f1..0ce1b9e8e350e 100644 --- a/src/core/server/metrics/collectors/process.test.ts +++ b/src/core/server/metrics/collectors/process.test.ts @@ -62,6 +62,7 @@ describe('ProcessMetricsCollector', () => { heapTotal, heapUsed, external: 0, + arrayBuffers: 0, })); jest.spyOn(v8, 'getHeapStatistics').mockImplementation( diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index b6eda2dbfd560..0819123138d0f 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -47,12 +47,12 @@ const packages: Package[] = [ extractMethod: 'gunzip', archives: { 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/darwin-x64-72.gz', - sha256: '983106049bb86e21b7f823144b2b83e3f1408217401879b3cde0312c803512c9', + url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/darwin-x64-83.gz', + sha256: 'b45cd8296fd6eb2a091399c20111af43093ba30c99ed9e5d969278f5ff69ba8f', }, 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/linux-x64-72.gz', - sha256: '8b6692037f7b0df24dabc9c9b039038d1c3a3110f62121616b406c482169710a', + url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/linux-x64-83.gz', + sha256: '1bbc3f90f0ba105772b37c04e3a718f69544b4df01dda00435c2b8e50b2ad0d9', }, // ARM build is currently done manually as Github Actions used in upstream project @@ -62,16 +62,16 @@ const packages: Package[] = [ // * checkout the node-re2 project, // * install Node using the same minor used by Kibana // * npm install, which will also create a build - // * gzip -c build/Release/re2.node > linux-arm64-72.gz + // * gzip -c build/Release/re2.node > linux-arm64-83.gz // * upload to kibana-ci-proxy-cache bucket 'linux-arm64': { url: - 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.15.4/linux-arm64-72.gz', - sha256: '5942353ec9cf46a39199818d474f7af137cfbb1bc5727047fe22f31f36602a7e', + 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.15.4/linux-arm64-83.gz', + sha256: '4eb524ca9a79dea9c07342e487fbe91591166fdbc022ae987104840df948a4e9', }, 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/win32-x64-72.gz', - sha256: '0a6991e693577160c3e9a3f196bd2518368c52d920af331a1a183313e0175604', + url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/win32-x64-83.gz', + sha256: 'efe939d3cda1d64ee3ee3e60a20613b95166d55632e702c670763ea7e69fca06', }, }, }, diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/public/state_management/url/format.ts index 4497e509bc86b..4a3d725de7e47 100644 --- a/src/plugins/kibana_utils/public/state_management/url/format.ts +++ b/src/plugins/kibana_utils/public/state_management/url/format.ts @@ -27,6 +27,9 @@ export function replaceUrlQuery( queryReplacer: (query: ParsedQuery) => ParsedQuery ) { const url = parseUrl(rawUrl); + // @ts-expect-error `queryReplacer` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` const newQuery = queryReplacer(url.query || {}); const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { sort: false, @@ -45,6 +48,9 @@ export function replaceUrlHashQuery( ) { const url = parseUrl(rawUrl); const hash = parseUrlHash(rawUrl); + // @ts-expect-error `queryReplacer` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` const newQuery = queryReplacer(hash?.query || {}); const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { sort: false, diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index cb3c9470c7abd..8ec7ad00d7926 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -252,10 +252,16 @@ export function getRelativeToHistoryPath(absoluteUrl: string, history: History): return formatUrl({ pathname: stripBasename(parsedUrl.pathname ?? null), + // @ts-expect-error `urlUtils.encodeQuery` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` search: stringify(urlUtils.encodeQuery(parsedUrl.query), { sort: false, encode: false }), hash: parsedHash ? formatUrl({ pathname: parsedHash.pathname, + // @ts-expect-error `urlUtils.encodeQuery` expects key/value pairs with values of type `string | string[] | null`, + // however `@types/node` says that `url.query` has values of type `string | string[] | undefined`. + // After investigating this, it seems that no matter what the values will be of type `string | string[]` search: stringify(urlUtils.encodeQuery(parsedHash.query), { sort: false, encode: false }), }) : parsedUrl.hash, diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 35703212762fd..3eff6edef33bc 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -222,6 +222,7 @@ export class LoginPage extends Component<Props, State> { http={this.props.http} notifications={this.props.notifications} selector={selector} + // @ts-expect-error Map.get is ok with getting `undefined` infoMessage={infoMessageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} loginHelp={loginHelp} diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index c2c82639bf7d5..11caab837a766 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -118,7 +118,10 @@ const normalizeTrustedAppsPageLocation = ( * @param query * @param key */ -export const extractFirstParamValue = (query: querystring.ParsedUrlQuery, key: string): string => { +export const extractFirstParamValue = ( + query: querystring.ParsedUrlQuery, + key: string +): string | undefined => { const value = query[key]; return Array.isArray(value) ? value[value.length - 1] : value; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 1901f3589104a..05c3ac0faea69 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -186,7 +186,7 @@ export const uiQueryParams: ( typeof query[key] === 'string' ? (query[key] as string) : Array.isArray(query[key]) - ? (query[key][query[key].length - 1] as string) + ? (query[key] as string[])[(query[key] as string[]).length - 1] : undefined; if (value !== undefined) { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts index 488da5025531d..c230e36e4c896 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts @@ -39,7 +39,7 @@ export const getReadables = (dataPath: string): Promise<Readable> => const readable = fs.createReadStream(dataPath, { encoding: 'utf-8' }); readable.on('data', (stream) => { - contents.push(stream); + contents.push(stream as string); }); readable.on('end', () => { diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts index b2eb645c8372c..b4cd9c361778f 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { promisify } from 'bluebird'; -import fs from 'fs'; +import { promises as fs } from 'fs'; import path from 'path'; import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; -const mkdirAsync = promisify<void, fs.PathLike, { recursive: boolean }>(fs.mkdir); - export async function checkIfPngsMatch( actualpngPath: string, baselinepngPath: string, @@ -23,8 +20,8 @@ export async function checkIfPngsMatch( const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); - await mkdirAsync(sessionDirectoryPath, { recursive: true }); - await mkdirAsync(failureDirectoryPath, { recursive: true }); + await fs.mkdir(sessionDirectoryPath, { recursive: true }); + await fs.mkdir(failureDirectoryPath, { recursive: true }); const actualpngFileName = path.basename(actualpngPath, '.png'); const baselinepngFileName = path.basename(baselinepngPath, '.png'); @@ -39,14 +36,14 @@ export async function checkIfPngsMatch( // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have // mac and linux covered which is better than nothing for now. try { - log.debug(`writeFileSync: ${baselineCopyPath}`); - fs.writeFileSync(baselineCopyPath, fs.readFileSync(baselinepngPath)); + log.debug(`writeFile: ${baselineCopyPath}`); + await fs.writeFile(baselineCopyPath, await fs.readFile(baselinepngPath)); } catch (error) { log.error(`No baseline png found at ${baselinepngPath}`); return 0; } - log.debug(`writeFileSync: ${actualCopyPath}`); - fs.writeFileSync(actualCopyPath, fs.readFileSync(actualpngPath)); + log.debug(`writeFile: ${actualCopyPath}`); + await fs.writeFile(actualCopyPath, await fs.readFile(actualpngPath)); let diffTotal = 0; diff --git a/yarn.lock b/yarn.lock index 28f032ef7122f..af1a1493bc36a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1489,15 +1489,14 @@ async-retry "^1.2.3" strip-ansi "^5.2.0" -"@elastic/good@8.1.1-kibana2": - version "8.1.1-kibana2" - resolved "https://registry.yarnpkg.com/@elastic/good/-/good-8.1.1-kibana2.tgz#3ba7413da9fae4c67f128f3e9b1dc28f24523c7a" - integrity sha512-2AYmQMBjmh2896FePnnGr9nwoqRxZ6bTjregDRI0CB9r4sIpIzG6J7oMa0GztdDMlrk5CslX1g9SN5EihddPlw== +"@elastic/good@^9.0.1-kibana3": + version "9.0.1-kibana3" + resolved "https://registry.yarnpkg.com/@elastic/good/-/good-9.0.1-kibana3.tgz#a70c2b30cbb4f44d1cf4a464562e0680322eac9b" + integrity sha512-UtPKr0TmlkL1abJfO7eEVUTqXWzLKjMkz+65FvxU/Ub9kMAr4No8wHLRfDHFzBkWoDIbDWygwld011WzUnea1Q== dependencies: - hoek "5.x.x" - joi "13.x.x" - oppsy "2.x.x" - pumpify "1.3.x" + "@hapi/hoek" "9.x.x" + "@hapi/oppsy" "3.x.x" + "@hapi/validate" "1.x.x" "@elastic/makelogs@^6.0.0": version "6.0.0" @@ -1847,7 +1846,7 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== -"@hapi/hoek@^9.0.0": +"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0": version "9.1.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== @@ -1912,6 +1911,13 @@ "@hapi/hoek" "8.x.x" "@hapi/vise" "3.x.x" +"@hapi/oppsy@3.x.x": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@hapi/oppsy/-/oppsy-3.0.0.tgz#1ae397e200e86d0aa41055f103238ed8652947ca" + integrity sha512-0kfUEAqIi21GzFVK2snMO07znMEBiXb+/pOx1dmgOO9TuvFstcfmHU5i56aDfiFP2DM5WzQCU2UWc2gK1lMDhQ== + dependencies: + "@hapi/hoek" "9.x.x" + "@hapi/pez@^4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-4.1.2.tgz#14984d0c31fed348f10c962968a21d9761f55503" @@ -2002,6 +2008,14 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@hapi/validate@1.x.x": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" + integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@hapi/vise@3.x.x": version "3.1.1" resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-3.1.1.tgz#dfc88f2ac90682f48bdc1b3f9b8f1eab4eabe0c8" @@ -5262,10 +5276,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.19.4", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^12.0.2": - version "12.19.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" - integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== +"@types/node@*", "@types/node@14.14.7", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^12.0.2": + version "14.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" + integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== "@types/nodemailer@^6.4.0": version "6.4.0" @@ -18818,14 +18832,14 @@ lmdb-store-0.9@0.7.3: node-gyp-build "^4.2.3" weak-lru-cache "^0.3.9" -lmdb-store@^0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.8.15.tgz#4efb0341c2df505dd6f3a7f26f834f0a142a80a2" - integrity sha512-4Q0WZh2FmcJC6esZRUWMfkCmNiz0WU9cOgrxt97ZMTnVfHyOdZhtrt0oOF5EQPfetxxJf/BorKY28aX92R6G6g== +lmdb-store@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.9.0.tgz#9a07735baaabcb8a46ee08c58ce1d578b69bdc12" + integrity sha512-5yxZ/s2J4w5mq3II5w2i4EiAAT+RvGZ3dtiWPYQDV/F8BpwqZOi7QmHdwawf15stvXv9P92Rm7t2WPbjOV9Xkg== dependencies: fs-extra "^9.0.1" lmdb-store-0.9 "0.7.3" - msgpackr "^0.5.4" + msgpackr "^0.6.0" nan "^2.14.1" node-gyp-build "^4.2.3" weak-lru-cache "^0.3.9" @@ -20344,21 +20358,28 @@ ms@2.1.1, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -msgpackr-extract@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.5.tgz#0f206da058bd3dad0f8605d324de001a8f4de967" - integrity sha512-zHhstybu+m/j3H6CVBMcILVIzATK6dWRGtlePJjsnSAj8kLT5joMa9i0v21Uc80BPNDcwFsnG/dz2318tfI81w== +msgpackr-extract@^0.3.5, msgpackr-extract@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.6.tgz#f20c0a278e44377471b1fa2a3a75a32c87693755" + integrity sha512-ASUrKn0MEFp2onn+xUBQhCNR6+RzzQAcs6p0RqKQ9sfqOZjzQ21a+ASyzgh+QAJrKcWBiZLN6L4+iXKPJV6pXg== dependencies: nan "^2.14.1" node-gyp-build "^4.2.3" -msgpackr@^0.5.3, msgpackr@^0.5.4: +msgpackr@^0.5.3: version "0.5.4" resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.5.4.tgz#c21c03d5e132d2e54d0b9ced02a75b1f48413380" integrity sha512-ILEWtIWwd5ESWHKoVjJ4GP7JWkpuAUJ20qi2j2qEC6twecBmK4E6YG3QW847OpmvdAhMJGq2LoDJRn/kNERTeQ== optionalDependencies: msgpackr-extract "^0.3.5" +msgpackr@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.6.0.tgz#57f75f80247ed3bcb937b7b5b0c7ef48123bee80" + integrity sha512-GF+hXvh1mn9f43ndEigmyTwomeJ/5OQWaxJTMeFrXouGTCYvzEtnF7Bd1DTCxOHXO85BeWFgUVA7Ev61R2KkVw== + optionalDependencies: + msgpackr-extract "^0.3.6" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -21311,7 +21332,7 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -oppsy@2.x.x, oppsy@^2.0.0: +oppsy@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/oppsy/-/oppsy-2.0.0.tgz#3a194517adc24c3c61cdc56f35f4537e93a35e34" integrity sha1-OhlFF63CTDxhzcVvNfRTfpOjXjQ= @@ -22732,7 +22753,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@1.3.x, pumpify@^1.3.3, pumpify@^1.3.5: +pumpify@^1.3.3, pumpify@^1.3.5: version "1.3.6" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.6.tgz#00d40e5ded0a3bf1e0788b1c0cf426a42882ab64" integrity sha512-BurGAcvezsINL5US9T9wGHHcLNrG6MCp//ECtxron3vcR+Rfx5Anqq7HbZXNJvFQli8FGVsWCAvywEJFV5Hx/Q== @@ -25422,10 +25443,10 @@ shelljs@^0.6.0: resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg= -shelljs@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" - integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== +shelljs@^0.8.3, shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0" From 23dccb7c1cd9338910a65d5a7e73cda61ecebe30 Mon Sep 17 00:00:00 2001 From: Alison Goryachev <alison.goryachev@elastic.co> Date: Wed, 2 Dec 2020 17:41:19 -0500 Subject: [PATCH 077/107] [Snapshot Restore] Fix initial policy form state (#83928) --- .../client_integration/policy_edit.test.ts | 48 ++-- .../sections/policy_edit/policy_edit.tsx | 18 +- .../server/routes/api/validate_schemas.ts | 8 - .../api_integration/apis/management/index.js | 1 + .../apis/management/snapshot_restore/index.ts | 12 + .../snapshot_restore/lib/elasticsearch.ts | 89 +++++++ .../management/snapshot_restore/lib/index.ts | 7 + .../snapshot_restore/snapshot_restore.ts | 234 ++++++++++++++++++ .../api_integration/services/legacy_es.js | 3 +- 9 files changed, 395 insertions(+), 25 deletions(-) create mode 100644 x-pack/test/api_integration/apis/management/snapshot_restore/index.ts create mode 100644 x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts create mode 100644 x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts create mode 100644 x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 7c095256bd10f..28d4ad5aceb2d 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -124,7 +124,8 @@ describe('<PolicyEdit />', () => { const { snapshotName } = POLICY_EDIT; // Complete step 1, change snapshot name - form.setInputValue('snapshotNameInput', `${snapshotName}-edited`); + const editedSnapshotName = `${snapshotName}-edited`; + form.setInputValue('snapshotNameInput', editedSnapshotName); actions.clickNextButton(); // Complete step 2, enable ignore unavailable indices switch @@ -143,20 +144,24 @@ describe('<PolicyEdit />', () => { const latestRequest = server.requests[server.requests.length - 1]; + const { name, isManagedPolicy, schedule, repository, retention } = POLICY_EDIT; + const expected = { - ...POLICY_EDIT, - ...{ - config: { - ignoreUnavailable: true, - }, - retention: { - ...POLICY_EDIT.retention, - expireAfterValue: Number(EXPIRE_AFTER_VALUE), - expireAfterUnit: EXPIRE_AFTER_UNIT, - }, - snapshotName: `${POLICY_EDIT.snapshotName}-edited`, + name, + isManagedPolicy, + schedule, + repository, + config: { + ignoreUnavailable: true, + }, + retention: { + ...retention, + expireAfterValue: Number(EXPIRE_AFTER_VALUE), + expireAfterUnit: EXPIRE_AFTER_UNIT, }, + snapshotName: editedSnapshotName, }; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); @@ -180,10 +185,25 @@ describe('<PolicyEdit />', () => { const latestRequest = server.requests[server.requests.length - 1]; + const { + name, + isManagedPolicy, + schedule, + repository, + retention, + config, + snapshotName, + } = POLICY_EDIT; + const expected = { - ...POLICY_EDIT, + name, + isManagedPolicy, + schedule, + repository, + config, + snapshotName, retention: { - ...POLICY_EDIT.retention, + ...retention, expireAfterValue: Number(EXPIRE_AFTER_VALUE), expireAfterUnit: TIME_UNITS.DAY, // default value }, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx index 7af663b29957d..a119c96e0a1ec 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx @@ -64,8 +64,22 @@ export const PolicyEdit: React.FunctionComponent<RouteComponentProps<MatchParams // Update policy state when data is loaded useEffect(() => { - if (policyData && policyData.policy) { - setPolicy(policyData.policy); + if (policyData?.policy) { + const { policy: policyToEdit } = policyData; + + // The policy response includes data not pertinent to the form + // that we need to remove, e.g., lastSuccess, lastFailure, stats + const policyFormData: SlmPolicyPayload = { + name: policyToEdit.name, + snapshotName: policyToEdit.snapshotName, + schedule: policyToEdit.schedule, + repository: policyToEdit.repository, + config: policyToEdit.config, + retention: policyToEdit.retention, + isManagedPolicy: policyToEdit.isManagedPolicy, + }; + + setPolicy(policyFormData); } }, [policyData]); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts index e5df0ec33db0b..7a13b4ac27caa 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts @@ -26,20 +26,12 @@ const snapshotRetentionSchema = schema.object({ export const policySchema = schema.object({ name: schema.string(), - version: schema.maybe(schema.number()), - modifiedDate: schema.maybe(schema.string()), - modifiedDateMillis: schema.maybe(schema.number()), snapshotName: schema.string(), schedule: schema.string(), repository: schema.string(), - nextExecution: schema.maybe(schema.string()), - nextExecutionMillis: schema.maybe(schema.number()), config: schema.maybe(snapshotConfigSchema), retention: schema.maybe(snapshotRetentionSchema), isManagedPolicy: schema.boolean(), - stats: schema.maybe(schema.object({}, { unknowns: 'allow' })), - lastFailure: schema.maybe(schema.object({}, { unknowns: 'allow' })), - lastSuccess: schema.maybe(schema.object({}, { unknowns: 'allow' })), }); const fsRepositorySettings = schema.object({ diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js index 5afb9cfba9f5f..7b6deb0c3892b 100644 --- a/x-pack/test/api_integration/apis/management/index.js +++ b/x-pack/test/api_integration/apis/management/index.js @@ -13,5 +13,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./index_lifecycle_management')); loadTestFile(require.resolve('./ingest_pipelines')); + loadTestFile(require.resolve('./snapshot_restore')); }); } diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts new file mode 100644 index 0000000000000..f0eea0f960b4b --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Snapshot and Restore', () => { + loadTestFile(require.resolve('./snapshot_restore')); + }); +} diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts new file mode 100644 index 0000000000000..932df405dde12 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +interface SlmPolicy { + name: string; + snapshotName: string; + schedule: string; + repository: string; + isManagedPolicy: boolean; + config?: { + indices?: string | string[]; + ignoreUnavailable?: boolean; + includeGlobalState?: boolean; + partial?: boolean; + metadata?: Record<string, string>; + }; + retention?: { + expireAfterValue?: number | ''; + expireAfterUnit?: string; + maxCount?: number | ''; + minCount?: number | ''; + }; +} + +/** + * Helpers to create and delete SLM policies on the Elasticsearch instance + * during our tests. + * @param {ElasticsearchClient} es The Elasticsearch client instance + */ +export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { + let policiesCreated: string[] = []; + + const es = getService('legacyEs'); + + const createRepository = (repoName: string) => { + return es.snapshot.createRepository({ + repository: repoName, + body: { + type: 'fs', + settings: { + location: '/tmp/', + }, + }, + verify: false, + }); + }; + + const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => { + if (cachePolicy) { + policiesCreated.push(policy.name); + } + + return es.sr.updatePolicy({ + name: policy.name, + body: policy, + }); + }; + + const getPolicy = (policyName: string) => { + return es.sr.policy({ + name: policyName, + human: true, + }); + }; + + const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName }); + + const cleanupPolicies = () => + Promise.all(policiesCreated.map(deletePolicy)) + .then(() => { + policiesCreated = []; + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`); + }); + + return { + createRepository, + createPolicy, + deletePolicy, + cleanupPolicies, + getPolicy, + }; +}; diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts new file mode 100644 index 0000000000000..66ea0fe40c4ce --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/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 { registerEsHelpers } from './elasticsearch'; diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts new file mode 100644 index 0000000000000..575da0db2a759 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts @@ -0,0 +1,234 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { registerEsHelpers } from './lib'; + +const API_BASE_PATH = '/api/snapshot_restore'; +const REPO_NAME = 'test_repo'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const { + createRepository, + createPolicy, + deletePolicy, + cleanupPolicies, + getPolicy, + } = registerEsHelpers(getService); + + describe('Snapshot Lifecycle Management', function () { + before(async () => { + try { + await createRepository(REPO_NAME); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating repository'); + throw err; + } + }); + + after(async () => { + await cleanupPolicies(); + }); + + describe('Create', () => { + const POLICY_NAME = 'test_create_policy'; + const REQUIRED_FIELDS_POLICY_NAME = 'test_create_required_fields_policy'; + + after(async () => { + // Clean up any policies created in test cases + await Promise.all([POLICY_NAME, REQUIRED_FIELDS_POLICY_NAME].map(deletePolicy)).catch( + (err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting policies: ${err.message}`); + throw err; + } + ); + }); + + it('should create a SLM policy', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/policies`) + .set('kbn-xsrf', 'xxx') + .send({ + name: POLICY_NAME, + snapshotName: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignoreUnavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expireAfterValue: 1, + expireAfterUnit: 'd', + maxCount: 10, + minCount: 5, + }, + isManagedPolicy: false, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(POLICY_NAME); + expect(policyFromEs[POLICY_NAME]).to.be.ok(); + expect(policyFromEs[POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignore_unavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expire_after: '1d', + max_count: 10, + min_count: 5, + }, + }); + }); + + it('should create a policy with only required fields', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/policies`) + .set('kbn-xsrf', 'xxx') + // Exclude config and retention + .send({ + name: REQUIRED_FIELDS_POLICY_NAME, + snapshotName: 'my_snapshot', + repository: REPO_NAME, + schedule: '0 30 1 * * ?', + isManagedPolicy: false, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(REQUIRED_FIELDS_POLICY_NAME); + expect(policyFromEs[REQUIRED_FIELDS_POLICY_NAME]).to.be.ok(); + expect(policyFromEs[REQUIRED_FIELDS_POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + repository: REPO_NAME, + schedule: '0 30 1 * * ?', + }); + }); + }); + + describe('Update', () => { + const POLICY_NAME = 'test_update_policy'; + const POLICY = { + name: POLICY_NAME, + snapshotName: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignoreUnavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expireAfterValue: 1, + expireAfterUnit: 'd', + maxCount: 10, + minCount: 5, + }, + isManagedPolicy: false, + }; + + before(async () => { + // Create SLM policy that can be used to test PUT request + try { + await createPolicy(POLICY, true); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating policy'); + throw err; + } + }); + + it('should allow an existing policy to be updated', async () => { + const uri = `${API_BASE_PATH}/policies/${POLICY_NAME}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...POLICY, + schedule: '0 0 0 ? * 7', + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(POLICY_NAME); + expect(policyFromEs[POLICY_NAME]).to.be.ok(); + expect(policyFromEs[POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + schedule: '0 0 0 ? * 7', + repository: REPO_NAME, + config: { + indices: ['my_index'], + ignore_unavailable: true, + partial: false, + metadata: { + meta: 'my_meta', + }, + }, + retention: { + expire_after: '1d', + max_count: 10, + min_count: 5, + }, + }); + }); + + it('should allow optional fields to be removed', async () => { + const uri = `${API_BASE_PATH}/policies/${POLICY_NAME}`; + const { retention, config, ...requiredFields } = POLICY; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send(requiredFields) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + + const policyFromEs = await getPolicy(POLICY_NAME); + expect(policyFromEs[POLICY_NAME]).to.be.ok(); + expect(policyFromEs[POLICY_NAME].policy).to.eql({ + name: 'my_snapshot', + schedule: '0 30 1 * * ?', + repository: REPO_NAME, + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 04b991151034a..c184a87365977 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -10,6 +10,7 @@ import * as legacyElasticsearch from 'elasticsearch'; import { elasticsearchClientPlugin as securityEsClientPlugin } from '../../../plugins/security/server/elasticsearch/elasticsearch_client_plugin'; import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; +import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -20,6 +21,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [securityEsClientPlugin, indexManagementEsClientPlugin], + plugins: [securityEsClientPlugin, indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin], }); } From a5dd5b6998163274d4fabf3710b4331d440e68ec Mon Sep 17 00:00:00 2001 From: Sandra Gonzales <neptunian@users.noreply.github.com> Date: Wed, 2 Dec 2020 18:33:27 -0500 Subject: [PATCH 078/107] [Fleet] EPM support to handle uploaded file paths (#84708) * modify file route to handle uploaded packge file paths * update messaging * improve tests * fix bug and add test to check the version of the uploaded package before failing * fix similar bug for getting package info from registry when a different version is uploaded --- .../fleet/server/routes/epm/handlers.ts | 61 +++-- .../fleet/server/services/epm/packages/get.ts | 5 +- .../fleet_api_integration/apis/epm/file.ts | 233 +++++++++++++----- .../fleet_api_integration/apis/epm/get.ts | 19 ++ .../apache_0.1.4.tar.gz | Bin 582173 -> 582747 bytes .../direct_upload_packages/apache_0.1.4.zip | Bin 607403 -> 607436 bytes 6 files changed, 236 insertions(+), 82 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index aa6160bbd8914..05060c9d863aa 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; +import mime from 'mime-types'; +import path from 'path'; import { RequestHandler, ResponseHeaders, KnownHeaders } from 'src/core/server'; import { GetInfoResponse, @@ -43,6 +45,8 @@ import { import { defaultIngestErrorHandler, ingestErrorToResponseOptions } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; import { licenseService } from '../../services'; +import { getArchiveEntry } from '../../services/epm/archive/cache'; +import { bufferToStream } from '../../services/epm/streams'; export const getCategoriesHandler: RequestHandler< undefined, @@ -102,22 +106,51 @@ export const getFileHandler: RequestHandler<TypeOf<typeof GetFileRequestSchema.p ) => { try { const { pkgName, pkgVersion, filePath } = request.params; - const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`); - - const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; - const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { - const value = registryResponse.headers.get(knownHeader); - if (value !== null) { - headers[knownHeader] = value; + const savedObjectsClient = context.core.savedObjects.client; + const savedObject = await getInstallationObject({ savedObjectsClient, pkgName }); + const pkgInstallSource = savedObject?.attributes.install_source; + // TODO: when package storage is available, remove installSource check and check cache and storage, remove registry call + if (pkgInstallSource === 'upload' && pkgVersion === savedObject?.attributes.version) { + const headerContentType = mime.contentType(path.extname(filePath)); + if (!headerContentType) { + return response.custom({ + body: `unknown content type for file: ${filePath}`, + statusCode: 400, + }); } - return headers; - }, {} as ResponseHeaders); + const archiveFile = getArchiveEntry(`${pkgName}-${pkgVersion}/${filePath}`); + if (!archiveFile) { + return response.custom({ + body: `uploaded package file not found: ${filePath}`, + statusCode: 404, + }); + } + const headers: ResponseHeaders = { + 'cache-control': 'max-age=10, public', + 'content-type': headerContentType, + }; + return response.custom({ + body: bufferToStream(archiveFile), + statusCode: 200, + headers, + }); + } else { + const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`); + const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; + const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { + const value = registryResponse.headers.get(knownHeader); + if (value !== null) { + headers[knownHeader] = value; + } + return headers; + }, {} as ResponseHeaders); - return response.custom({ - body: registryResponse.body, - statusCode: registryResponse.status, - headers: proxiedHeaders, - }); + return response.custom({ + body: registryResponse.body, + statusCode: registryResponse.status, + headers: proxiedHeaders, + }); + } } catch (error) { return defaultIngestErrorHandler({ error, response }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index c10b26cbf0bd1..9b4b26d6fb8b3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -98,7 +98,10 @@ export async function getPackageInfo(options: { const getPackageRes = await getPackageFromSource({ pkgName, pkgVersion, - pkgInstallSource: savedObject?.attributes.install_source, + pkgInstallSource: + savedObject?.attributes.version === pkgVersion + ? savedObject?.attributes.install_source + : 'registry', }); const paths = getPackageRes.paths; const packageInfo = getPackageRes.packageInfo; diff --git a/x-pack/test/fleet_api_integration/apis/epm/file.ts b/x-pack/test/fleet_api_integration/apis/epm/file.ts index ab89fceeb5b49..2823b236c0321 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/file.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/file.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import fs from 'fs'; +import path from 'path'; +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; @@ -14,79 +17,175 @@ export default function ({ getService }: FtrProviderContext) { const server = dockerServers.get('registry'); describe('EPM - package file', () => { - it('fetches a .png screenshot image', async function () { - if (server.enabled) { - await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/img/screenshots/metricbeat_dashboard.png') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'image/png') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + describe('it gets files from registry', () => { + it('fetches a .png screenshot image', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/img/screenshots/metricbeat_dashboard.png') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/png') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches an .svg icon image', async function () { - if (server.enabled) { - await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/img/logo.svg') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'image/svg+xml') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + it('fetches an .svg icon image', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/img/logo.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg+xml') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches a .json kibana visualization file', async function () { - if (server.enabled) { - await supertest - .get( - '/api/fleet/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' - ) - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + it('fetches a .json kibana visualization file', async function () { + if (server.enabled) { + const res = await supertest + .get( + '/api/fleet/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' + ) + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches a .json kibana dashboard file', async function () { - if (server.enabled) { - await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(200); - } else { - warnAndSkipTest(this, log); - } - }); + it('fetches a .json kibana dashboard file', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); - it('fetches a .json search file', async function () { - if (server.enabled) { + it('fetches a .json search file', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); + }); + describe('it gets files from an uploaded package', () => { + before(async () => { + if (!server.enabled) return; + const testPkgArchiveTgz = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.tar.gz' + ); + const buf = fs.readFileSync(testPkgArchiveTgz); await supertest - .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') - .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/gzip') + .send(buf) .expect(200); - } else { - warnAndSkipTest(this, log); - } + }); + after(async () => { + if (!server.enabled) return; + await supertest.delete(`/api/fleet/epm/packages/apache-0.1.4`).set('kbn-xsrf', 'xxxx'); + }); + it('fetches a .png screenshot image', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/img/kibana-apache-test.png') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/png') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); + it('fetches the logo', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/img/logo_apache_test.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg+xml') + .expect(200); + await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/img/logo_apache.svg') + .set('kbn-xsrf', 'xxx') + .expect(404); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); + + it('fetches a .json kibana dashboard file', async function () { + if (server.enabled) { + const res = await supertest + .get( + '/api/fleet/epm/packages/apache/0.1.4/kibana/dashboard/apache-Logs-Apache-Dashboard-ecs-new.json' + ) + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + expect(typeof res.body).to.equal('object'); + } else { + warnAndSkipTest(this, log); + } + }); + + it('fetches a README file', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.4/docs/README.md') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'text/markdown; charset=utf-8') + .expect(200); + expect(res.text).to.equal('# Apache Uploaded Test Integration'); + } else { + warnAndSkipTest(this, log); + } + }); + + it('fetches the logo of a not uploaded (and installed) version from the registry when another version is uploaded (and installed)', async function () { + if (server.enabled) { + const res = await supertest + .get('/api/fleet/epm/packages/apache/0.1.3/img/logo_apache.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg+xml') + .expect(200); + expect(Buffer.isBuffer(res.body)).to.equal(true); + } else { + warnAndSkipTest(this, log); + } + }); }); - }); - // Disabled for now as we don't serve prebuilt index patterns in current packages. - // it('fetches an .json index pattern file', async function () { - // if (server.enabled) { - // await supertest - // .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/index-pattern/sample-*.json') - // .set('kbn-xsrf', 'xxx') - // .expect('Content-Type', 'application/json; charset=utf-8') - // .expect(200); - // } else { - // warnAndSkipTest(this, log); - // } - // }); + // Disabled for now as we don't serve prebuilt index patterns in current packages. + // it('fetches an .json index pattern file', async function () { + // if (server.enabled) { + // await supertest + // .get('/api/fleet/epm/packages/filetest/0.1.0/kibana/index-pattern/sample-*.json') + // .set('kbn-xsrf', 'xxx') + // .expect('Content-Type', 'application/json; charset=utf-8') + // .expect(200); + // } else { + // warnAndSkipTest(this, log); + // } + // }); + }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index 53982affa128c..a6be50804aa5e 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -71,6 +71,25 @@ export default function (providerContext: FtrProviderContext) { warnAndSkipTest(this, log); } }); + it('returns correct package info from registry if a different version is installed by upload', async function () { + if (server.enabled) { + const buf = fs.readFileSync(testPkgArchiveZip); + await supertest + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/zip') + .send(buf) + .expect(200); + + const res = await supertest.get(`/api/fleet/epm/packages/apache-0.1.3`).expect(200); + const packageInfo = res.body.response; + expect(packageInfo.description).to.equal('Apache Integration'); + expect(packageInfo.download).to.not.equal(undefined); + await uninstallPackage(testPkgKey); + } else { + warnAndSkipTest(this, log); + } + }); it('returns a 500 for a package key without a proper name', async function () { if (server.enabled) { await supertest.get('/api/fleet/epm/packages/-0.1.0').expect(500); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz index c5d3607e05cb8cc6673a4e75ca08e4104bfabf6e..4dbd2f223d5064c424d1a1e34cb735e07e46f447 100644 GIT binary patch delta 476506 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⋘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 )Jughx3#&-6r_gLDlypMR5QA$lnTyvBc)zQRl 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̙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 delta 475954 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 G)r&uog;XlqvO7z|YSpS_t zp0=BpY42}nFVc#9KAe)PYy!&mobXj7$+I+nk+?IlN!AVf@62g6t_6sSZnP%5%(e8H z?^HK;(N^i9R`=w+`>DS`lh?zV&sCitl%|t~d)5U>T%kv8@q+kFEXzXcH~FF~6vet! zHWF!y6lVrj?^N!4F)iAX{Mt^X3FA6i%SQY)Mb$T@`EmDWGvBm7JQa&I$h82+SvU-T zhF*~WHlJmy14)&0aeSl?N;i3VKtka9*Z_3K2$xz9ce!spdF-fNKDpjjFURk_KJBed ze(`JLWA}?!!7kaRpEg+y$qZ4O#T%Q+a-d_UCs<g#bIem};jiCvMXbPoU};0o-|a${ z)rh2!yw-ZwvfFxOR_(-lmhnm3Aq+u(Nqm#+mtWy=yy{fXo52i9&Df0WPb1)FMUKq6 zIoEL4tg)=-aa`@CII~T`{Rq=3>r^hHXWHjj;(vq;1yGhZh|%I6i(^9sv#usT<Sfgb zB@E;ubp-#gvWVmrKfUa}T<wKq0=r5&NlpP!6-NJWqm=_+SfW6o`$VG_x`g9@#K&)n zNlHMoRpZUmJc7o*5UYYW@pitMK;`k~@rb=2O!OkEbJDkxjuF**t*4BxnO^-9<=-)= zZVvLHsr9g+5JlK%J1zyi#43|+gg~tPU=Iuz-ft;<DUM)-rCU0}+lnX<>?C(dobdGb z-B_J3na|eP;ptmI=Sm-eRi+nz9aQMlgq1m<*b{YTsFSNh1x)kTyFubcY;z4Sg{XW9 zkhO-#r`^FRKCUpn*FW9MxyYl{e<OML)*)mxSBC(peuyZ42>+8jCcZ#$4)G6^qn>@$ z4hu;i`JKPgK0pufIh2prmY+L(ih|1%%&+UjAP)LCm`RvtKxN9~V<!}Uha;~eIXkk< ze;g(GrJ2X4_)l<tx0;J3O3KJN@E^-!MLIFb_Ps>EQ|dLRHU@kirGKNt)vh|q@-rvT zD*a&zRkrU;hM13|Ob|vbw$l)yy;BlK*Xy2K?rk3hjHNl(jMr>h)u*~aACpfjH0g>9 zOSQwB6vZqL-IY21GNgrnEo3_eELMERiX@#O>(^0BD4Mk^@Lg`4l(2Y}T0rwUnr@Fu znb-ej&c<xAf5dF}wCDnO)PCt4Ra74s=nF<Sx@5d*&={0ZGc=rS5pWK8i`=*0?*Soz z1u-Qmt6wRMa{U_wdGy*?4RD>a$37({hRxla=W-DZ1iK=ny~B=w{kB`Fs4F}89zzFF z?si!spa#<lt+Hh%<%AhVeFOd4^W|8R*X-J%edUDCSrt=*1ccnWJBi@92#;Bewx<CI zn^%5@9)$j;=bi;RH68u+oYE@$+S+_c5nrZvOjwR4u0g9>T%gXo%`{nmxpq|M06~<i zJjXX^Vp|sm=?U3?cG%0m*P@=EY=$R;e*L(6eMRFyCKj!eH{8zt;H8R!TZ?R>J+4;u zxb`ZIs2|CZ`O@?G|7yApwc3j>R?HHBtp(zLJz{78Mk~km!f4XhAZ-2%NumgtH!Lfu zUYASH_rDj392x}5J6ExpdVSH(vC!@4czVHGzr%kf8JK&2WiX~Y=+7|USh>{sx)L+l z=n;k7pfGmZDs|8fe=K_Y>Fz2c&o3gKrmgK$I!@$sYz{AG-(K56RpkAVcTeu)b#6rG z{X%E`tNrP6p`q%|o6=4K{?5A^%mkoD)o*-FRMut&l`-BjrMWFR#m3~yyC3BAT_Fw2 zPZQHui_oTjU{)NQ)Gy8M0zJKksYcQ_(#D8S9N#gvq@J(j88C5R?0LLbYTsbCjz{>D zM-z1IK?H2(ReJU+aI|fH#|f^h^(a3=Y?9aIm<Y~$PDQv91!BCwUEZ{nP#vP_2iGGQ zI2tq`W2Sm(Vd)B`hsJ;W=I?eySQ50>##;7b9;Fn2*tApNm|bEk(JyhFVbm#mrjb0F z1C;+)(=791l4xi2d96WaEOw`1PfEn99f+KeP0Mr@wpbR*{ev5l0EOc~@YTv}I{m1i z-ss{w$j8|%e%N~W-NrS*j?b;8I2ecZH+LN5O*JhIFPgQ9#fSmvM4-=PrQ7H_cy{k_ zbZKCJWkPBgXD{e|cEx1?ZZ0B62P4AnN0L?&zbUN@^YWQP84zKBkXM8OgMbgv)Q{Tf z-l4GlcypbR<S%s2dS6}?5i`7MlBQ}*>I;zw4Hf%d)|=w}v<<Z=N7wYkyjwvw!9$gA zlr#Euy{>cSB<Il2kkvX0lccH9uqYi5nFOqVgva7PExRqkc1!vcEaWLkE%l~-l!E1V zuS*<{GK~AKC8;DS;~aMLpMJ97EA47@5c7daa0M2N4S$k(efSp<XT|)5MZwy=WhtS` zPs1T7aey}&>%g7x{yX3&B<}KqGU8HtkVShoL&&CQz(VoxZ!es}Fwm~>TmEA7Zj9M~ zb!3qla`_&Eje=ld#GQXbxIcA^it&Z(p7?AYjcF_FP4nwFxJ)u`!xTC$?f1^Q4US5> z4IUX&*8?}_HNgPKJVn~Vx+feC(~A}y()}?3XDB%PHo`=JKRf<*U%t6fmcM{4N8Df1 zgI>~Fd5(>jFr1QSO?OFcK+WcJ8s9g6nJP$-l=!|VMdz2AD@l(t%O=)`*gJk}i^zOS zVu1#mpDT45hQA~fNH=|SW>QY3DmxtO=-2Hl48t{KjmTu&v7TNbOlOGxK4a={Ntv{% z{?uMU<Do)bpHAt#=#`TEN6SMOulhcI)!&$#GgtnUn?97(D6*tvd+<zhWLtTED_mHe z$v}%Z;GPbV66QMUi)x;jL5<1tsKk_WiSXMkN(E$(;!3f1Ltcq|-8H18+MHRJJgjc6 z_N!e;&3Kmg$*_uESMGKzSYYN#${n|JgrXyt{jGRw74qSt@fL7h?&8layd%D3KFA-+ z4Xg{J*yWFlY~r7jmm)GZ=_>Jm<x5XOc++S~K(>z6=#<a!V_MTfu3x3g3TT%=SEbak zs09+5PGbPK<p7)L<cJ~3vga&LiA0r|e&VVF3p^!E+I&;+CMHPL1hhT_N#era??A*Y z{T=<DU>chmpRrq+SvMjQR4Rt9*ug!9=(<+4_>sm(pod#M=U;I8iyuaRQ;afb2iesb z@{R726xw_66}tZ>_Aecmtwo8tzIei9@x6yA_7CPpqHd@teXnf^7atx0gp5!wRnEe~ z-4ZM+&WOh}OqcX}=x*(<U=czFl*`W!w5j&^5}CYF*eS2H3n_4d%45Wp7L;Ij7)z<T z{c!WUXka43wL1L7!u?x+Sx;9lHw+5q;}KP_N$5gLFyB4e9FKJDOIF-<u008U`8N3F zlTmW<Br;sRY15HJ8}EgqBV=XL6gT7p<1HIMWJK-}3o@b)8MrBbV9W7nnS{<e8PxC& z)rjb)%4-&ildJ$ju;03lUe@j(`R+4jOFe0N`R<hJyTNw0QBQ?`(U;cWUWfBxD?bG) zP036Vwpj5{s_aC!JiV#keLE*7-^O#5d^NZWUM@-nYJ@K%jPX>cy&}#ftv6z;7@R-{ z#Y)i0Yuu}~iE2ffbf>zE^2u^%Qup%@4*9L1jy~y%0u#Ju3F2?@aO|b<xc-l;vc8$i zFzfY?lMOTz=1u;8URj%3yAf_?YZg7iQUQI{({mGFRTvbB1bc$|2kh|w;MC75uE3cZ zxPFn?3h=0Qy-TTAQLGF1@Ec^enx35ayM`uP^84eIo>pm`^ztNg4%kynE0&W)<xw0v zeq3cynm@ikCVuWUB2@mnY-I@46)>$O1tl<OaBLvN5wE#_I%*C4D>!^jwv+7856K2R z-i3iy?=NK%w~+C%Mc(1+tRF*gIv?+u%G&^qDl{isSuT3b{JY=wNm%Q4G>yQmwn<%I zvrw*hBfy&-a(rbRw#7CEwY@i(*z9t6YGYT~uPStcTd%DPUN;;LymydiH|E>@AfTUk zRvR(yc89uuP_4_e-uZym#2>Sem{!(eg0tgV$xkM&A1A;SyC;Qj+UBM8BhM=H%<PiB zJSWR%i##D@-(DxNd%^SqeuJaBtW`5v;y@R}#HZI}o4i7JH}_L6et7)DP20J*KO!qv z*u3mBPQS=Rb>g;1dqzcH@w{$E-_($q=a(XSD#Y%8Yub(S7uljGG_dv8F)3Z_6Tq5H z&Jqc9##ZK=6bZ2Z$8THlBI~rT?$+?u1IJ0>kMH1gDC9EelfFqHrKAW(&of*1Ga04H z{d3;^NV6Ioln_{TXNp>?U!H&*566#nUKD?<Q9qW>tLEtV@!_~f=6+IU0B`3l)#r;? zdi;KWCFsf;xiU$wB~Po4R`%Vm^*pX{(4CepArVhE(yU5^zj0eDlnWqknNwndj{}G* zEm6`Jz@?nbF4=TV|G-$$6$E)<aiFUW<)XU+`}m=SAvj(tzkC1wh*+00qn~MO;;IOR zLt40ijMTZE%9}5N!k1aLU^pbxIQFXkY=2jOT_H2As;rVexz7$t-AFak?k}!-a;>u8 zGr1|n7gsT5+Kcj-ypWaLc~|qTCGY!M%M`a`M8b*c<%y=L9P)S7_6jNYlUBvr70TZZ z^KXq8m`!<UP8_ASvs&wNeD#Sddu6_=YfB;4_N;o<-8}EmuBt<V8BN-2A>j@&ZWGsk zipCPphgT-tPk@yDN+(^_E8PJjY<4%<$tO5wy;X&^jm7n$aSv@$XXS?aa1>yERd|UE z{6%q{Z-_66C{N|(CLP`1#z`9elakmUkoe+RvTrLp)E@;%X^<Tpd3i7a^?U};9rpVD z8J9ulXQEy!>61M+fe(M{nj3((U!wSbUq`e@#41JQjZAF+WVWmQlg?+LA@<%mTxjC! z3Ql8DOOU%9(tg1&T{|7{mC0_BX%W(1)Pz+FER&$@FH=|gDVkqbT`<Yj4g2%VZqDJI zotgZQL8A!bxj*Sa9xss^UdkA@oJur{l7kL!(tG{v5})O(2~x^0Dp!S7$_O!kkAWv% zzQhMIriy2O_R$FMFbjBWM4GV=^YGqoLz~Rxxa@(xhVjD>A$C#mqZTDa;?A4lNi+F* z&Q`1Vtfnru27*${?MMjQxzUnpZJ$?~ZH@Y7XsYeqJBw^yuT2o|hb-)WrQeP6Y{M^) zgE7o6SMSgSmv<n<i;jOn>=e#_&V3lvf_(xsydF6kv_4`?fxN#OZR{ka@<@peB-zJf zowxj#z47kvIbd5OSuoAHRRDXptGaibJ5LHkKA51c1$cELS3sH>n9i2BOiHoV(n^hu z?crxq@1ElC8!wY^oE*BC6hnxnJy0^sJ(@e*EOBR-I(}c&={WxC98FArZr-voG|AWk zq_5cJ6oQvjv;DDVk8L>eD->Dfn60CWe+$k_rm7*v5DPXZUoL$%u5PQ6(ywgVZ$Z$3 z=DQ+j!*LRV`W9tGE>)L}EudxCvZ7}>dnJ}fj{(s=Uv+{>i|5lxr2X0zBv)eNu(a*y za|kS~q?;rU+oH7v?!hmA$}4*1+7G%I`h`54D!B6G3sqR`7`0+L)j*YT6#YW355Ma- zmHiX1><ri$_#=vb33BZJ3j?plA5np~ojs4I{GCJ`2sn47(b&@iMW#dI$E()3L;h!2 zjy3c)`u@~Y`0tFLTecmwBJV34-FSYz$l)Pq>w_iAjH1<0f^Ug`hkKgT#RErgpzrs5 zX&zHMZb6K|DbvDDH}FN3UMCe1cn{DKsDhldVM?B$#XbtjiFgV>%ka2gLU&!(i1X}| zv)_&&gRG3G{&N*opZJM`b>uct#R$LG7g+hFvW}AWSI_0{rY~{Sh3NzrncYk5+n|Sh zx3i?;axagncz}L?+a%{V3$;hKn!-PGx>p4pj7vBs&zQ^!9LJqZ4+xGEM);}PfD%O? zmf0t^9dCa={h#|o8*b}OyR_2!*L`oGK%HO-wYjR$N^F&$8_+ajEA4Vr%$rf>c*zWM z^*IEhKL=)n`@@P5q3TF!hRfQjv!&cxNHJui`|Itca9K%z#4&>4=fyAlTx_-*`W9cG zo_w|%a?rk7ldIiJ0sw&+<-k`tai6>BxnJMCsPv#e@cT46?8tm}@nWzsj_4z7k#gds znPKtR^7=({f|9U06|LboR^ksVxT*wvPj(X;BIV7Y+1I$lj6^o-j3fx@cHV2CD$Xr6 zb{%i*t}QixCI@*ar<3T0^>Bvs`&`oCoFw+yDF?i_tXCT1*>U>-DI@yJGwE<STWS&U zh@z)`Op4s&%jrd7ticS}xreaaHqKVf`WW>Id2nd>T?}cg>=K^u1(V2?4s_AB?ypJ{ zXUJ^ejbCLf%SbBfdbCPk_K!rq)AXT?`$zakeqDxtrBtr)0A6dG4^O=I<O8qdy~=(T zc=0Yv^xr((GEo(P4A>h=r^}6OkX}dgcCvZY#^8=CCp80K@I{8rLFJ=m;ME)=!0i2D zwVeXw=g7RX{w$~OCmYWVXItCedFt~C?dN!-RE&So^mdvt@b-s!ht<EkTB{_1V19aW z*ReW(M`PbZD}2LK^&q0b@cDrxjvj}~PEGc!S1yB5Zx8#+>Bz8SDO~LR+=pztpM@U= z-XuN7voHDy{fJ|y3I50#T;;HIQ=jKDWxvF8oncq+1^~e?7}mMg<mhZhv$b0>JU>L@ zM#SY3IcRTX{m9sR+KZ#RS7ie}*}-S9Ya>X15_y>5dnfL{08yB3WJziXEpK6W+8AII zQ$?UJMc9rRV{Nv-IMR;c=<pa@`u{edBTb=4CxIjhMij^(cX427M9J}Sy(!d26bkp2 zQH6IlAKMiM{4W>~!f^l^*GFjh`PpLSQ!HqUFe7sE@@&p7*IjF6M8vA_+vMy0lDZ3j z8<M0;W0K*8ccMJIU;Y;&<hgGA`?TzLugs|ZP<NdVa48H`fxrgb`$Drjg986_3M|R~ z*lMZO(4Lt`w^pXR72Kds{aB^4?Ii6`I2K9AwnT)LKg_!G+PB_=J~do5chaL40Xc>m z6QKV)i^}AOo}0@yj|o?`RvstrK9zNUAI4wDsG0y;ev;JUgz)oYMw?dx>O=nOLi;8E zFadPjGO{20xBWBS`%It&j>yIQM}05P(57~aXA=B8hY?Cba4T!2^R`{aX9NP(0Jod$ zsY5VCzPuqxbFjzSA7=UQD747jf9Og3t4)pox7&ShAXUOf{~}NQTG8$d!M>e;Z}z{n zQ5E=lp+MYv0IzQ5d)n#F?II!V^ayX{<VXKpj{-LlBYi?VN5^1m5H5bN_0ptZ)2izS z!gkNnw0-g80aw{fwS&l4pJY{{3{EMGWcimkd4b^xf!<K+L#Fa?qY$^CKUPdrvA5N; z!+he>s&@`Eh0@ioc`strI?-)^CbVhgWMm0qfytycTV$c=GU9tB?!)3x)4dn$q38!C z@xXrX?adWwAl!AVTvb-e@v9($E|r%bERGz<S)J~o!uK9CtySQ<`mkdK6JSG5ypmMb z^)jN>9wtQ?Hx&|MZp|eq$MW#Tj}hvdBsBa{%=IU2a5m(AH|u8?@#kxQ$Eg(PB0m-c zZ8TtyQ@MJtH>0$F7KM=c!gvXztL&tm-`=C@i-mX{{4RRgb>`k2cp#30lp(29whirK zNARZN3=dLr!=Tb8nMOReVpNTIPHDlgsIyMiG5W0c2K8;Je0sFTX#)Gnu}%TiHkH>9 zSfv3O!O?=Zz}bger@k70O3I{OJc+<5dPRBa!IOa3Z-)uvx=O$JuZNHTeGWoR&{IcO zJ>%9)UW&-RfbDIQ12NyXXjWlaLTo3!nQ!h7tAL(sJbhi?3kG7u!R)I50evj&dHDc( zNHm&P9lo-r)KR?gK{}H12rnXpA4G~<k-0L2`E3ETF)pIz?*!d{$^epc{7V(JRp|k{ zGd<S#!p4kc$+CW@0uTRXkpGu{m$loTUHcY+?F<nt=hDX0>g$``6T%Z!4@>JGvA690 z(fDttI6ykw8`-nb9!LGpWTGy~*zdo#WydP^`Dzny4OHefABdJI$vr}X%uYj%a2flU zV$5J~6LwnWFD;pWJ=Cw?U0qRgIEbPWF|2DH@|7rKiE)4e20G2EFPbAT!b`DP7)+t5 zI+B~LPF_-z_#~^>!~kk`$vHuc)qYE|?I!mCMjFUxSj0(+DWx0lPa-K`=um3U&<;pu zMNMiDx<)`e24M6yg*T#vadmJ^UH`1@Z9XB%9&h_f0uHW!Y^6x$jhNXjtcxQW(enG< zz)<24zO|zN%l}D1HoZdWv=<&ZwRg31PDL`)EZ7b@F*T%g`zg`O!#cMv)C*Ak?!NXj zKV!wNj3LF<>54{{%B$xqG{N^%E1n}jy^LIBB~S^NG*atN^ef9oeL{Q}^c)_+7;a7V z=-;OJNm$%}kLB)e1R2Kfbxh3PaazN0+{%$XMiHVl-cY?OzbIZTq~rRG1Eqrh^~NVH zIp%Rg#28*49aS_ZAO-q-k{?}c!3eJ#OcwY2KDJwN)qry3m279I_xMn&3c~B8<lStx zqx*BA<a%-OE_1`vCVUGhNvo)9Yai~e_gvjYaFud@*KD7}TWek~nq!tNi{;&%#}>Aa z;<tv&E{y`|)$KoO&*882RvJFx)-=}Z0E$XVwG_(>o!DDH53jd)8b<bAI&S)^DDuTI zN|230CFmNQ7W4tkuBQijli|J>zc@nIcXma+enI`*K?>c};KrTzorh9TBAIIxVnmHf zjL%DdSzN4Anm`Ost5Rh<=)C_IWksV+-cr>cK~<UlO}8<kR@c>Fy9VZHzx5a$24TM3 zv*m!>%hltDBNw*khI&FVYWTtu0WF}JiYm?i6ErqRgdFk*t8S;JJYtC<tqUC;`e+y{ zr~`+zcM1iC;KB%Z7sPK|B?Co5u7zA8Sm0`Z_AJbuBUQk+zDZ4U&0fqJXk9HB=N|&* zCWIpCw{rc4roH|kwog}is<5mSqh6i6__~P)e7@1+8uZMY^!SYE^O!hQ5}&d&)N?=A z<RyO->W#HbJ>+x54EZt;0R8=!-gQu=g=<6_VebEtY@}U!N)^48SyLgx1}ZGeB86Um z&C6KI0C|odPa2V<3g^*|CWccQ?9R95HqUo&-n=lhs_}e>BfQ`#Y>MuWX?CMSp=ym< zM{I3;eIrzdhw;kD_}<tQJ{;(ejoY^EIVz+}+Ujd8qoy`ll4dN29)8toG8%r2h-E;u zpN^Jm2N;mI1-p1`(P&Hk7);y7<L<+MV)Ei+iEiI<H)MF<K}tnOiTG%n(>+^gc)i+H z`le#`N4%-rqw*D=j>6hf1_h$<JE;FHWOjgeHBvPzox@C}DN%Cs#~qpIv=V609Lqr& zv>M61s-M4Po0(>6<xyW#Tt!x~@xrZq35bU~@(bV0vdQPVbP~`&5>@myKIN2uVj;pd zb0VcKvdaFjXRXgC^nHOXpP>?W6t_uR4p#V95y0a1<;R3AA1^=q$+o}14-4y%+aD)1 z??o0!Tb4=<aQw24R*Sn9n-4`3=>KXk%_pD>XGbSR<4+{(r!}*p{0FWTYt@&LKqVRY z)DSNI{#}S}0r)~rTuqaG@nEih7FN^OUfeyw-ZKUAj3(|FVIqV|kWDO3y^2On)n%SB ztg4m>vrw5v(=)4KJ-yi;%}o;6$#m2&*HmK@)6rMs*%`|Zh6j^~Q$ZSjCqoE{0T#KI za-jki$4LPp%!)HF&}&z(>FiU*8V1a7nRTEUit@kv++wrXT<of&-i9B4mrs1l;=(YZ zQ&hCtCYskIp4hyGZpqUmlwdo(NsfyV+_Fo4np@{QKs<g^?*MBkH=ukid7!a0t?J3} z)Z*@8={~3wYc*SV<VL&lLcJM?*<#glNK9v*8_*);IM=3;$*Bd8zh+25Z46Al`Na1O zFz)UQH{^yR9@nXB0zGtpMB79|ZE}6zAJ&{|R4oSE05!#QELq}i^xM9%{Fqu=Y(^KZ z&XC12N;6&@%e-tCE5~4sZuL2|Pd6B2{0)^Qzxlz{rE##%p=KbnHLZ<Jg*WY1ovR4c z%3hcQ)CnKUHO2mDHAs^*O%p)4DG}pbDDg7{@>E<sSyypW<ud+%D#{DvgPDtlSRm`` z+btjUt(uEvuAVAtu7+&x*YWW4q+l&;@f56G--Vt6MlIMp;~TBNvKOIBnv2U6x1NkJ z_K9pt*C#9w&pV%KQ%LVJd#m(*OLu(9YPMqKE}}~0@cnbe@gHEnh^qM4?hmww=|uHA z+Dy^S?$d1wAIS}WXoLaj4fw{Pld3OLV-%@hfj~4wy!LQg^PhEZmqSHmlku~SRJhv? ztrh*Ta6+tWUYOZ;WL61^e(GqFgZ0vw>1I>FUJopI07pQ$zpHo6(fpbKg?Dd0;R7qX zoeQgMnu>|oS9aS9tf(v(4wOrwE;!hs$yhiPxQRR!4g2pTLQ7VEe{VZV(0p}F<oD$y z(gZ$#+e%h3Ce;6wX;6c7M%MpxVe|a6=$AzjR@|$5Rm#GOXyV8<_P3xxjb4!>=Mv$P z=nX-$OoQ8Rvp&*tGu8ER`og((=NKbVhjNf!FSXlv)IfHFVSe*`^UCBG(zy%L1vSb{ zmZnM1OCDI{p<z^sf1NVo8K=b~4%Xm*<DDnIFKO=Dxy-CfLvc?e=0PJDW==-;pB4BU z;*|ZW<~YcKvnxQo{#quan|rS?p>-&b0kZCLyjmc6Lms9gWK|8#di3&tfXtrtl3Tmr z%MI0?Q>w7?3@!4^{#;(nS3qXeZim7>@gtQ9mFiRkj{H^=e}%gWWBNGP*8RPf-}pFd zhS|(aanxU#Wq+A9c5*-UGa}U6dOG9TJK=n<6_~?EpMe!bf+q8?xyD9g*U4EU?CYza z-mOrwpG^y4zJWC=#rKVKI1-tq8!-`r%9=FlufLBJI7hH9d6sHxv&u9HHcYwK7T$*y zlLzM-wRYewe-R2`%6QV_?6~YVUH_W2fvvJA_MC3Ee0Up-6B`?_pM|ug$rmb=ZuHzI zH#=VTH@j^(^ek$HOj^I#MAAH5=RSU+P4+$XQVJzO-fhuo!Ds@E!oVM~+`jiN-$$^f zx3C?`%JhE^Pet@(+?MRqiL2LKj4#HXQ?U3zt9@bfe<SI8I<}E4f7E7(G6iW7VI?=2 zWDmj3L_~8S199X6YjnAlfs^jVU<bfe+)=;t{R8PHWn56&*1Q{5!CEG>%fmc9cb0>4 z)<UdX&PDo&Ny1ypB^Iu`y#yy5Z;cwhpdfa+gO6S#S=4-kPj;#w0)Ayyo|-;Glh6Y! z$yu>Ye~Yy%izqOeXkPOH72j!7e0$0BjaDyH*$zlK<s4K^0HSe&Sg&m*y`jc?B8S(% zQHeJx^^sC>xz}PX0%Sb|`Y2?tCFp2r*88$zeeju@ICn(khUOZlttJ!?dKNceK8LLt zLSP0kEOM7O%mBZ~V7!7z82(Xsk*8{9Sn#$we|k%zDOg1$Q_;}kI?CgV1Sboz{!0x$ z7?~lRGFnzzzhh8R1yoRa@`8M%yZ%$``!z|bOyAoN3lQnU_38Er7^@T0c^V6&7(3sn z*Y%e;!|H&hM@!w=+P8!yiisln5T<h`ukS8Ow$eHmQJRc;fww>V4W66BpW*M!9wfQ> zf7yBDhE@ozdJfFW1%_Qrfki!Ko0QjwdW6+RpJR;ae?NjcD;bi8S0JmNv5RJi_s`V+ zFv5fytm;fuK~R0$Y&0}ZFWchZ_5he%PRZndc8T}us){dK#wA(G8AtamKKbrIxn!;M zK}1Wq&6nd8(r=d~gm;vAJnYbBlz@Y9f2^3rmh03I5E+RD@a-K=^Da%oWF9*oY2Vjj zTpr9og<`72WfDZ|LsKj~)A)>tYvHD@50SBH#QW~4QkTwA6<RjY<^j_A>py4?w{_=z zkNs`?w>$51`tdum8x&a$ds1g~=?$?kmSAD+s}goU?$@YY=AUI*C|SG?Pg(vXe_otr z?#l{3N|(+28k0Y@TELN#980r*`fQp*oZRnMu69-@EO6(z!)}q!N!NS!Z7aKP<Vnns z4vwc_zXB>OBQK@k+kj-b0v!<xSqtyvv-;i4iAlHIEKY=-sziyu*EjepPgHQ7+!Dh^ zzjl%D#hn8F==SU`i)g@MQ^Pd5e<=Ge*K>sW;CAIm^_$Z}Ws8792asyUXpt?b32yyB zTk>j|Z6sH+z~+U1jK73OnT%n=Z{YCZ^3sCYcbu-t7EkLB;TmEti>9qw<Rheq>C*Et z^X=Z12XBVO&LN+71Yyc@=<hFqNkwfAdeTIqahlu6YaA_g27z=yfCx>Jf25URbY_1k z&QMQ^{5OOf0n=B289eN=><ROTP~R7&ft0~jtmh_fjBz%uIm{OI(V^jDOq+N~eN*eJ zGcb|3E#p-CreXnj&-#}F2NB%{dhNIT1?+CMx|Hm(r_H5lTcPT^TF-Lrt9UBxGV5)P z`77ow#%(j@2nPjq$djb7e{A2J7VR>!6A<nFAx#PlO#{`D6Uw0W5tE{N<-9r}RtwFO z;dP|_!nc;2Zog=aO1n>OkJ>12wo#6iY4(78+A><;lqsYsnDEe|%`aXQU=kd<^T8(E znTMOfGiy##OVLG}=Bg%^<!ENnqQF^lbE|E@9Zx)!$LJNyZBbv&f41jmf0AtUp4A&W z=Pake%Ldz+=Ub^%Wh)S?myBAyKw3i8y>Au?wk)cxbYg4t-hM6yu`<WK??4;*!t+4z z@qLvmYHf_Kywut5do^B3z}#oY6zy5xKtB^t5B|F;l1I&7DRJX=zz08rs1tGyweV+j z)7K@xAC9D3dw>i}f48e7kFwXv2-4E`JWup~fDEg&w!~=<Z*JAom-rid&fAXzR75nh za4h;PkPrSCL91e&=lwr_hV+E2mWu@zNc?iwX86}e9xXOYZh^=7?9)qPj<-86%9dEK zd&v!8X(mS``!khU6aYnLF(sxkGTM0676{o{__&dKqI-Mbe}H?^_P06~|7Qxh*%Cr( zW4~keLwi=y;*M#K*hZRed_*RljHB6|j=@f{-7^`t<1^BXZhDh>s1%MXb5)3{>`wC< ze34*bVX4T&I$Ro;c(*1dZ>(fNgxb?P!hd3V^I}nNjbxUat~o#p&KmKTZ-M;KaFBe) zb*ewzzN!rse^K+6B^xd5J02If-A?OLfm0f;E~3j=v4Wwt&cOXWYmGK)$xzFO?&o_n z-0VU5y4LxC#a1+o>n7my1uoytZ-*F1QBw2ZH(+1!Vd_aiU+gs>oeD#u$J<Ag32bu+ ztGFiH_0`4y`3qoVbj*WcgoUgTL$K;3_AeAe1j@~)e_QdXYWK#uk>BnWb`q3D6$@u! zZr;k&CejFZ<vH~I4rTlsD9?32Rw*T1>TPCPAV`L!ztP@KuH=v9#4M^=vrQQ(U1@*y zDPnBQ{_PnD@1r)aF-Af7x@61Wq8go^658S2PrQfKFm_qAeUo+wCFi(nrp^%Pl|YG9 z!<UZPf56`PFReFcHEe`KUZ~gO9oP5Wnk$*?*||r{)ca0qt#wyx<)9PXd6~d<C#Gf5 z*C|!nkAqP<+@oN(aw`5cw?K%Oh~MvSM)u4Zm?zIVt8jOof9O($y%@iK7Yvcgn*|HH z$2}BZ{+*J<awqAx;d*sdpbiuBFy!&niHN|Ye-3vIb%BO@)*aR%i_9js!_SoxqU&^B z>vw1-q(?dhB+S_P)1Odn5j4Cc#~hp8Hs=CD5X<k!pS(!axZM{!+WCNV=Vf2d9$SyU z0KCwV@z0mJTWu$SsqnY0{4iwdo~aDHZx|sLbJR7V>VJA|#dPkDx?^kQbu&B8=lQuM ze^tyHdyzZQ*1&c!Tf;n>XW##uziY%m40d}QNYUWpZD6DCp&q<KnCX8pH(KR${+is> zs@fN=)7b#I^6o_ExbB=VGsG<N``T}N%2~UBcQ;*zmbHGFl<Cz<(kDGx?|hgEB&cjV z)zodb_Y`f8Sj(v&xKO2IBrt@BY}n5=fA>IFHivX#IIM4-*cjLLga1VQxi+5l-KoOX z(0Cd9o&&}?k+m>x-G&K`deV1$r#yG2EJo+2I>1IP=Fv;<ZIsy*RqUO$5_PG%e+%38 zB@XqEh>gHfyx}d^+XFQgaYf?eoMf<dDqupklX^=(6T|!bVJ#}1q@9U$BxTy!f5I+^ zna<{8Bx=%SYJzb{cXwAug+w>A5sq?MR?BoyF8SDO^1UIYgS?e$B<I3T?P25|pnE6U zaa}s)lKB(Lq_jGd)%o~xI4M3#dA7l-eTm?#$Qj*zF?rf2{bmqiFbMPRdG{<JC&#Ge zu%su{#OtpbX5|Y_@4iv;)@OKCf9Y{Gt>2yy>7?h{dvhJVa?gH4&}N;LgGDvQ8;}(b zgWFb<>tlWim<V5ghQXxsOq*>0lk?~+Sn>+$eDjfUx3&HA)vy0}6_p?{xG{#Mxco?- zfT7enAvEd*pQ6oQC~mJZmrhQtTit6O?5W3cqud^#KpVIEuap5b;47)_e|w32oe!_k z&GMkN5G`Ouk-KFNIG7QEIyv5Lm9g=M-88m={m^GBC#NpN91J8TV4nVw4qx%mshNn% z1zBR2tL?g>#e%GQZ9WsStq)1HyTy!8)$D>mJ6B3spWH+L2upqL23DgkR?=^M=Yj8g z)~HGLPXNdThiMMSi>&_!f4nyawd%3ON_T4>!#8hhk$3Hy`kTlhJ7OLutcQ=8dg=au zu2FZjz;}}_j428<Qs>;3I~~SwYdYFETcB#VT<KP0r@vyR5F)0<>i7>zi;Q4tEW5Oa zDrwA?=L`dB@4?$`K5!?^TN2pA;o|6IWz*P258W==EkfWmSU1*(e>12bewv>@`y1!b zmPD<X{<Bid*q`g}3NDkPI#+6UR<qqIS@Q^&X`_x+QK?WAwKaik_G##Q%o1m^s3w$^ zSM5(&`(DgdrWWZO6UrvqG^&p#%*Fv9dIwK?$+HM{dOI}uo$F!kkKMSdiLt&7_G<n{ z`cm1&e?UX2ydtVCe?g@>uz3-OLw<gG<h;l>vd+%`YQ#ltnisD;SsQ)3q4~qt*k;8A zk;l?N%p$g(Rg^t+-Oxk&c{kLRCZSy3af10ESjU~4%b(_uFP7UvCB2@pEe#?UcM#<! zNb8K~?pf{q1u7CLntulWeW}u=aKoLZU?%J^Xk^0pcQ^lKe^5>xcb`iYOSs%@zo2KR z@m+VIUqJp>g$G4lQ}MyCQEh{a<hs|senXGE6SBKSesN<{rq{oUO#a<ZubX=Aq<djC z8d|!l^(<9&s3)}}&2^KmP}Ob~T!I8dmuUQq{r?sqN!Iy)h`HN2IO=uwJ5SpRoqN2H zN+!MYZ>jKUe`Zg{`whHb8*<fZeCIU?qzTtElgI1KUj@6V2>zf_{h=wVDHyt5O!`+y zFcwd!&y^WdK&xCQJ0tO|_6Zi_y4(f$p6`PHia4-9_Pcs!Hk{3|Mar(4f*m>)v)BAE z;xDuP5uhIB(Up>;?iyp)kF$k(j&1I-7(Kz>>ao(xf3!Odt5?`OIa_2RsTk|u-D?V# z<ADpWerNupOFZ~TjgPo#GS*$78+3=C%!})GwdH|1Q$4Y`pWtCXnN457;%Akq*tVAk zDO4v?A2fas<?i0=5$W^?XBNh>@FIj%&^$(y7Z}mtk9@z!vjq-1AWLi9Gx^VQ$A$S_ zn8Gbre>tK``E{RgXK6|#M_ejRD1w(Ngs@p?RHj3B>l{T)<ry$$$&7^JGOMK60lTe% zE;mW)7*yYFJPX4b!c1!v;9cpunuwF9E>fFqMWNJ(1#=uF0l9_Hnzp`cG6DJra6j~y zu-ZItitM+T)~9I6{<55PS)?YR%i^Reeq)PgfAsdz@bt-aD^qAmqZ2Wq<obxeilwD- zjw;T&=QxSpUdGMWnf17>xw&Q~e0A6il0#+Pzvx0kdF~J%<C0CG-{=0dK&mxc@z_Y8 z=kad)(G2}oIPQI`@9HdSeKmvx_~uYNx}5)Dq*J%36yz-Kdo=Nr_4&=g9NJ2^#<Ci0 ze|Rmpt`T&+jvE?uD0$v>xRtaz#@RbPhdEb2-v&tv)xOSD!$V#i-D@_gK_{HD*;+eO zTxk<9++&0I^Z{O~Ey{MHo$8EFseZ=gbPAgWTd$MTt~->wtVq{-m_3aom@KY_yx49Y z#xLGCC*I1$>rdxioTjya)(s29f!>2UfBvNzB>{$!Y0H`9<eJ7370fCNv&{*GAeJ4b zz#9dcWLK}C>Ev2Mco{yfjNC~dfGUi@b(?=weGR2-f_%Q56H)6KG%a>MZ1P>}J2hs{ zs?33fl|J!c5`)DL&+Gp|)>}qJ+5J(&79}O1bcmElOEZ*$G>n4MDLKFZBi+(Se~P3- ziKN6(0}L%g3`pnDFpLbHL+9iDf7iR#v)1$Zoc-ZiXYX_N@5H`ZDml->b_c~*!;+=X z<_8(A^W^+8v&zM}wwnLnt=YQb=5g+XV%w{uuJ5A8149xWXmawOWD^1(9p(wAIzCs_ zE>;*zu|L96rcM|!TTd9dY)iRWf0M^r1;87PAJkScyQjo65MJwdTT5C+#?5m)=v=%b z`GEa#7!}4+;=S-~mvuae%%QNGrH5gV)z@AAoRt@!J4`S6Uv53Gsx67R6SsF|wg26W zoEdcQKAq)Xf;b;vVN`0LkDz>-RowO}2ir=Ao8!tu!3%fO{I4+PTxzZ6f0ydgj|bg| zbi#BXwubHjEuzwcywRP%>sX;EM3Pq*JJ8KmSQE5{m@4uuw=aQKB*K}Eps1!5r-%!B zqQMSv6AD+hfJBvJr-sCrP*Me<sP2Ig*@$<H4$Feb!wqPm^k^2&8~*#5;L!NdrvmVg zs~3O2Sx(HU&hm7`Mlj(ye=qRN31jn_b~Ha<fg%JlDxG$@9J;g9?WRRos&~zWfgPIH zQ=bQO)$<i^XKGcvw_oTkP2Y`cXzB4<{ZA)s@$%|QrBU{1X8#l&nUe_@wpH0a-&kNd zh7HTYQ;Q{iUAaq;ZS~F|oz{8Glrc=Hb9sH#n);CkFF8p*<F-qmfBxpsuy<rP^<#dE z>H$GC<~50F^O<#2oSf(y1Hw-`lTgy5Q(RVG@jBXbHmm4e;L9nIFJ*$NgCKrACej}9 zMC!fa`b3Yc0bCzR$W~YwX5h<>o2TW|tK+4s5nN8BDaKw|(z*>l)Ql-1vvBV^D5SH1 z`&u({2kvgK6Km4Ne@|U(nIV2}zc*_NFq-uqoibtEdnG5@9t2dCqtdO&c+es<Aw<C} zMc;OKl(7Go1~^;fVQ%EQRk9Tl7FEb{3LnJakp)Y_pigSE$_^V^D!J--NBP9~dw^4x zk@^+6+Fz?8062GZfe8adLbXD8uYbtmyKOxvUP5&?6aMbZe^V>}Y227*I?iL^!en+l zKlT$YgS^Vum}zlhXSrN7XC6_s0;RSS)q~p5iP8NfG<{Gy<iG5@OryQ7wqF>`8~i=) zoX<8>!`jx1mEt&Cei;fQ{lO)>z*2>S)*N0C=KZR)axmENS@#&s%$0R<(hw8#Z~h~x z%&Lx|>tANlf4x-**bXOKpp3i=0>7OheSa2Pv*%fU3+KGTzmCdaQvQN`0rFKhjQH)C zK+?=P^6iIHB)137iAq+9W^9H!x|%*GB$5s(OydPQ;L5dM;{6eJ`Ik~992?2qcUd7i zZh35IinyOF)l1JsZoGpa4tqCJ1?swD&_a^_*<f_wf8~EjI(_#m_xA@7iu>&AmZ0ZC zuq^`0Fww7f(7D(c%5hkDnFx0T5F+5X^LT8q`vZuPB4spO_}!=Br!<f^PD*5^!ux$` z*o2|sFuDefLWB&*XIKi&J%E`zq~ewPI_m-NXqGo8n#+`<Tpr4y6@nvM{MjMpFr+90 zJ4qU8e+lP9`Dxn1FNENf#l96M?^N7p3tqtFp*-N<n;Q3@&88fWm}1K6{%Owf;lDQZ zg2rf}+2S2jaj8-ub)t-2Lp%vttmehdn;pfal<RbJftL{eD>WzBwwCz9b>5xG%*4%- zN#=!;F1v0I0XUUPnvdD9z_)9QX%#oeIcWJaf7v8?5{1yc-}Y{v2itbJ%zC-uupyJ4 zL81?%6Sern*`54=EdGStQm0Mk05>cH55u_hC#7aHM3v_2N&>5TiIE;`2EerO6BpT^ zlZA&f1&(lQ3Xj0Qep2i^g=;yBFoKVbX_QDAm-hhwU1ur8?dNR^_57TJ4_EVvPZ&n? zf7&m4eJ(kV30vvCop*o0OO%-Q2(sKROGbIXg7141Ay$fFOQ+p*P;2v16-QOWX0P~z zXT`T`00m06z@uU}+|l>_!!n_8@~y+VG})HR`$}Pq4J&8ZrRkJb4)(vQHO*7JJHlcI zMWIS+5aU(<<GS<@lJRm{HMhTS_UjYoe@_0m(1oIPSc1#`E|Xl^bmg4X8_r7oD$+8~ zl4h)}p47Z{o*n2mi0_sY8pbuuK1T3@{o~F!!ar7g_;jjO>uBgzzsxo16WzI9NnFAe zLn&E^@zfh8CYCB98&<5%<>*;3%kBXPs#VFFxX08RQKzYw^m9LlTE%+Y_wmvue;MBU zrcQ<nSM}htjk-{%SJ9p>48VLyQtgI>o^uAwx<EhV^(wdqCQB-W5JG}<m#$LxX>buQ zI+c6=9F)6$LW5RL-$ffDd0d8gwwA(OCDAFXTTD)XpTI)TE5w>E$zbsX^qcsiHT#v! zv(6lp=e5uP!Ts`bjggg5_GhVkf8VThlZVRmI=+f2(eN`Csn)f8pm|Fc_9^Z+=Pz29 z$Qee(x}^jujpsdG2?~Id5@D@7BtY3ZQg<H@({M6jdG3~guwTpLPeZDGIGD-CxyMKb z+<L=!g~FbU6}IGXFExsf9=>mkhtM8!veD>L0WoCh&+;`Pw#7fg%UHGde`yZlR__E4 z*XDfcA|at!)|GX$tTsf}Dc@)6dbsM=L5%(t`>C2$c}oqAC|g<80u9w-*MrOu4|xuM zMVF1m!()%77{sF51mB7^k#h9Y3_QqZ{>!vgpxp3S{|k=zKtn9?KIsvA1*DF$`##%P zmKfB;q-fi^Kw1^!P<GKtf24q_)6KPcMyYlCgy*3+BJVK9xruDOCa!i&ep3PRjLZQ! zv9u%<GH@_5H&>yKnubd#9~PA*-?R4=IKSyc#aqDZH!@NWb{#U4WD47sTdiH#Hc3<G zRHfMlwvx3RI9#nDi`&Vb@p9PpWXexjHeHAL^YGN?%kRy~m?v86e{x>~Bo-Z8@-L>9 z{CAS-4HCoOV7>n)P~oT;9^A?vMH5vwq8+00PXBuWiI;JdHU3Qt$>7e^FV@L^IM*PT z_lC>(`}lT=RC?6>6e7@!R6XHoYESwFj{8WOUIg{bZg3(#0zcdNngsxzTcq8+P_eX< z4Wo><rQ70QAtl$he@YOaf_#IVN<cYl&fny+rRne6naTb}KsJwl?W-<O^nnvxTMnSO zFXKpzl4@s9{8l)_^apQ%ZTWQ!e4VyW&~UR#Zi;Uw<#h4#E&dz8>+z$`1<Elqw01MI z_<W78X6LBS+UZ{8G1FA!T5hmDBbv0dh;m`{wg2rkC0BSxe}1(O{KOP`hfd1Poih`= zHB~u&6&Mh@CN_w}ck$jyw0EyI-G9@xAJdGPkI)SL(h$8jHjWY5H(zT%I-m}wa-AYC z@9dP+dL9E}%spjSj#9PaIT(4ztFi7it+yta0hWdfsk051V~dBT8K1gptG5fnc*pY- zq_;AAEmoKpe;^h9gZp3OQ}x?kXTN3Bad!}2tk&&Ispt;Kd9q<_n{Bs~|G1&CDBx-; zg|Sq1VfI4J#WPFReR0S)UvTi_aZb3dmMK`HAl_JpgXY=IBhxQLY0^=h(~mp~VZ^l* z+LD6OQ5_?8qk1%J!T|uh2AGq=^fTy1MW2~;+#W(he>gGIJ6k|FLn#){p_{(ashHIC zI4z3LiX@rH-Lw?*P~O*83nDOOz%FDd<<?rkq2o64crQA!E!KmzL0_6XhHs-->T~Xz zw2@dteJTj>_e<vUL4hP>VtT$YLx}C1BBKx~$Htejmj8B#dyFYiPjWr9JCwjKUMPN( z>Ft|se>X$^OABVv&2K6jp*dWbT7_WRn`4V$8wqtpQfULvQV}UgVqyBZ@F%>=bnssU zZFrd7QrU-CMQ(-tyPH#Hb$%`$mrsF*`M@Fl`yp?t=lUd3zh-!t04EbGY!m9@P(2}Q z7M}c~Z@M2$H|Z-lvcR8ywPEk6fJHjfNtA+kf7v{)4n|+BPcRo6pqP1Z4!5UBXq&iK z^8%p#R_Sc+_6kjnl>8(D{9YVUVUkPi+IpNRY{5vGDGAmYNeFa9@3u}6$rQ*bloftZ zO@AmsRY)J})&0Tj8S)uE4Q}tM=_8sZ-xum=b$T^YNUlxWyGuKeNvD$|M<@72^66K& zf5IS5N&19%DNxs2N^DN0fLv0_=R=EHrW;h~W;5M&Lk-J39&TJ)B-HYP;|yH87XBud zc6PKia4cpHVPzeozl*0~LPwU31@$%gvSeE=M(U7w`EB33m(cW#-nibwNMb$EAEiu; zc&7D}T;dOll#k-Q*g4EbrL39vH~J>Fe+iNI&iUEW?t-${1A2LSV*O2gFJo@|cyed8 z_y1zR989ot`MT#L0^Jd1)Vmh#@*l6#+YB?Kdb!>GLoA}TsaSIo0@s=^n7&z(6%q-O z8MWEG6Z6gcxRjP=d%NM@qgo2%eKIwdh}8oHa?rkL%W5wwH%p)%&e!ivzS<ube<7qF zvP$hyLcS06ov%(vWvv`*Dxrog>yqF=`khZ^Km2SQpF~IG&~2<hois*WzV&EuxM6s} zmDV6OP|E?_ee=8YtEtpeB>xe6U@nh!wctYG@>L-XyX(<L1H5Er-?yqo_=Y_9p=Nfl zyF$ddI~V;DWJc^<pI7H$KPh%;f1alWaEogT23{Zh-u!~KT3G}SjJBUY>#}?%bhPiv z8cENJZg0$8*&9x^Ol_m7WHmIW|JNV;wdgw$-YwNK0Hqyqf{|+4Llg&b85Zt!d1hSt zZirLkHvFNYHkKLo%%tDXUpijY6*oAJ>dksu<Pi{~g1mO;D@+Q4?7hsSe@YKhE1l`^ zlb&dA8-xMM#;?ggMMFBcDm+u+39sAPw7Qv$n>M2WOHpe7GU)i-@_lolFNxH^wOCxR zd%HP3CK}uJ3$v9f5)rgs_0KQO8Ll5Mw^2#q^++UU#O$fT^>-o?8x$(HTKrDxlOnpk z2Q^11qP3svAulX~5Be>>fA@4olL=S{AFXt2QojX*s+LIup0)p@4anzwm98_FYhBg) z<7NF$<o3uhJI&P(_1iH`3(*d`JGA_}BjXuD%;(J>o{Gk_blR3Fg?Z>oG|z!F#W)|v zDDliC$Ap{7h?zY!XMW1xgtFUOAtn=-R6#k0m&K3aOc)g+tw!bae^h_DOe2$hd;5jj zpWcW`GE09F3;5twY`PvgScyde4H=oM=h|;Am+g?1vd8ZWw>`q&r^D7OznP3ovfOM| zwzDl>c3yI?`+h|V(bCR$`52+vUt4g+p>9|znBZ5g3vJRxCj&j>Ji!+e@TPUrysK~+ z`%4SL19Z6&Sfah{e;{76;I5ecHt`e#HUZ8)Y|+9@n@MZoK3|#a{3KMHw4e4kyb}d{ za_MZobnLq8pIe+8quB))*KsHNR4>-6l))8p1tuxAk0^}DubYl&E3*EZO<kX?Bj=<s z%o7_xm6+w5+c^)!UPzv9Mff6Ge14xL^mjJsG9v*4fo+X=f7jhh?|@re&_n#%|3|<8 zvy2t&777O)b7H?3A(MPE8->ojr0P6e(h$DQnQBD^a7I{Ej2$5hpR6gnEs=a4ZkVve zu67XX@h%!#6#$us(GZWNcABAotc{@R$?n`I$!$_W%3h0UAAVw9@b;mlL&^+{uVgm! z4`BV9!pnGae~Q=SA+lg#f8ec-;caAMKXTg|gR(kfUBtwHPSTH|1o<^hS{j)WZyoH{ z?)OE0=$y1V+vR4;bw2M)mzA?vX0?OSN{ts=P~H#g%xnJV$}vQdk*{MX*4q!_(Suc+ z2iR+F;!f4D6qC}aN(;?D57<^S{s#ZxV6q?1mb^=Oe_nk@v35HiZNc{B26FDEafB$~ zP$Y$)WfyYDgUl4~FSh1f$rZ+T?zd8~$iK^2i+|c#Hi_Sd$67t~DOzHoJY|X8+n5)k zKYx#$v`Qeox$c?79|bsJHpRM5r3yq?oBYh%tiKl-xI#h9@C*c%bG1tzYvNq1y|<Ll zauvyVe{R6xm#V-^eZWaHHvp=VyjDa2cY>8FJnNciI#wT*2+zGfTFX;K9kd-BOK$%0 z%Sm`*HZlm7lz#bKK(JUSDO=}^U&k$agx1p_e-d6*<K~phzw!JkuF7d|(GBy{Wy7pa zH&kZ(!IRkrR#{3-Q-zyda6Up&@PXpKdd4%;e+8DjDX|1VnV{>s6wgVp%<NhO%bfSR zR|69!d+ByDJn1(EYjJzA<)w$sQ;HgNp~E7lz`)45KquySk#o_yyu;MWAg-&e_A3)9 zpLI*a2Cs#T70DOE9vxcIB>)Vk_wFo6<$AR16(UB&PmF)dbE410^;$yq;4dvh+ij_1 ze-mfGU{OeX5j*Ia*OP5;J50QvLSbwJMc<9Ehq2h3ZMb-;bG+7C&49Yw1`$(yDsyO; zsLaVU_uwAY%nfpa6lt)R&Pr*F-N#$*TQJXNG8d|3rq8iS>wdY$`4TX}m?OF;;_>Xj zbVR|{M)A_W!4iyE!zf7s`>|(|rzq-Sf2>cNk?A!6<Jdur9m0xJqg%w}t{)Rpq`D1` zMOK04`DIDn5yg84*ML2QbIV!X5$SxVkMTd<yuOqKDv<IO8p;6^qxL|)^L|!F0nuxa zvW}W_swphS9+OO77Rgst?*C6R=4#yfkLKU69*y$d&&^g2;I0i1X~f`ByMn?if7Qvk z$QpwJKWGl&26Q$M`L*9tBex^V=gQ~;s`&k#O2-R~#`hN;FIGxFRWWU@szJo9W3e3{ zAkR5$4;Ng4bZmy&rgeY;o0>QngXupkO+YJiTdzlxt_AO4N9WeyW`2km&v5AuhaN>S z`2pjre+KLamDq6SBq#@2k~uw;f1d}jyebqMuBWsN5Api_A~j4{uMQY0nXu&lvxCbl z=N<ReSm=KLebqr$a)VKY*1%)|H5Ui=qrTjma+Zr$D>O{#XN8P*NkI*j-ABON4Ms4$ zKM;fT9WS>G@im~4qt(9snV-_`hp*V*aSa?l49*IQ^eB(!Aez3lOwzhMf2hZLd=8>) z0*<xmp`XjEHUEC6vc~q`CE#F1K36s!K)~kxC6Iwc+eAmLoc0(hDXkcw<SFW?CA6=$ zDZs+x%i}uhwD%J>BBSH4O?1NcZ4^JWzpxjiX)IYwUfjd#4(H^%DNmXj2x7rB^JW^j z-fW_aNK7i1W0g1hTS;+Se}oH~DynU-XlQ?qq;~VVKS+SCz%)B!x*_QaQd6IZ+XPKz z*=|a(xxm0<5DU=45;gbeZTvi<uyG1p2=tHUeFF$Nh_sf_F*3xuGgEk*K_Wc|9-7!n zIU|w9-hmhP3)ItGO4AM7FCX9Ev{zK!47$SRg2sRLhktWIKb?<Re|<V|;kmWrx^xpb zyM3mH%n8IWkW|y22;P(Bjv5q^ux=`QNQg@Ln{i(cNPLsPEX^H;C+Z{>lBd(JG&b{= ziA&54yl6a->Ork}#NiXjlGtQ@qyn&+t|jE}X(Me!52T6H^*_)APzfDw`(xH_Z({ea z_kt@+Dl)X)f9~;df2L#?LfTBk9Gm_;Uj9@gx+WAE?_{8EV<exmYcRJ7RTxf)Zs>Cn zz4xZJk^e?<IJB~<%&GiwWyg7S+xHa!M<<70E|rc`n~1A+^BElXv#FaZ=r63X$Qj0) z>GTI_TAJY5a+=+AQGk0Bcbg&hlr?y0I8Pc>;Ld9qZnb!4e_+iaho^1$a@t{_N%gQE zMQWVlm&bmmyr1XYqdW98Z?ig|nwLNI+<ia@^vn>r7(_V65j7Xy`^=<u&?}@f|5G4C zmLAj-f`0opsQTxZEs%;D3EEj&)(n-ppE3%0qoyhiNf{j%=vMljPU$}TE`c9LA!;hR zdZ*{(0^S3fe|{Ku_E}rNw3RstS18sI_458&bObheZ2pjLmzUop!Oy`3tV}ijNf~4R z1y=Vbsh)B_;aE*Bu`uxQigrohM3HX{-^rsE)><%6A>Zn0y%x~ju<*oc)j+ys$%y-c zJ!jNpm;x{HwNTn~qiR}S<M<sVtgFr^;-c4SZE}_ff6}<{_b04AcW%^9MUNLnu03tU zd6#WWq#(J5u45<1OgDe+;n`rO1Ss9NEv}Mc?jwV^Uo*dwl`{2Cj?=5@ai%G7bdR?P zZb)&zz^htea^;S8i09Un;T(HAs>2N7(M_h%04XbKNV3a)&r~9JG9u5KF28Zbafp_p z)eYL)e~PE)$wnt}pG#HtW7#Omx`?X9Ywga@w#63O{QF(%l#(9@9Xy@io|?gAoQ)o? zYH|`c+2F&)x&QuR%dDT$2l)+DAKb^>i8T^?K11B%uS)hVejb$Y+i|uP)Knf#8t2#? zn=TYVx+WC9mU~&cp!LOSt5L>9{mS=c8s+tjf6vooChDgh!=w9#6U|TgQxwaTyvlK& z@VO<^g@;2w>A2&$iF;0>h$=z&6&;_8GGIgy=(yZy&H(Oh3zznq_qoIm%Q^Y}wHi1$ z1=r+x?aYeTDtMN+TS#yMY~d^&<Z><bOpL7GybkDOkZ7KY+AMrvU9y8>G{|bQ^n+D@ zf7)P8u2=yn<xn(h+188B$&Qfy{NT$kgG@wXIWF@E_U-9<0e*A1A>ona76QBeMpHc8 zb-oGIASq~4-!)c3Td#B+`x#p!ST5~VZUK!YeW)_EmET$%_(q~9Bev$Y+G(ldUHaok z2`7K?1@uBN8pLw~jMX?n?P^rsWlMegf7&wDoZEKl*L0@(jl?i?r1sMte77I2a;PDD zai4AlTN0Zob5Q~%QN094YQC%;;T=?3S=P<j^=15V$9gf!J!AM?@$A)cP+aOHj^~-L zk%ig@l*kFjtGV++6ODt`z$H$C?6co0{HxMYtZUYgIV?!K$I?)Z<a=sI$_l60e{N3Z z4`q%gfcp9$x`r7~2@SQcKL{5@hu2m`h&|;di}zqrv4(fMg}j2=d~^L+F!bR|lzTvL zvX9i>8E+f`>3u~iNz6*J56XT8k(M6zB^w)N!5_s<#0#ScgH0VgMA-EwjmgG3Rhwc9 z&a+LZ=aXEm#+DIw8^%H~kaP^<f61)kL0}dFS4WvNF$>Ih$f3Ac94Z^lYRYSiPOryP z!}8~RxlB=-czEDr5Ptw`r2H3&!X4t6NH20PD>MtCzvsH2SWYRyfPg@06<|EyI(8T5 zH82S~D;+^M7^9-596BcGF7)Ygj@vX;J!%?sI!No+X+`4Bb{Cq3$_*O>f8NsrgI}C$ zJ!_RqpsOsaX@nflzRg%Dee?AK08XnPmy)OVHLC?mp!%*vTC1^~r2K1vvHo~EzsZX~ z@tGW$7-v2f84pf=(`l0`Hba4)za9Uk=8qDUDk>}5j$2bO7I!=P7<&<?P~MrT@1n&Y zWZjFlITq9Ozdz7nOvy-Qe>XL3v&m!ehw;$(2tn&L(*;Gy8^tJ$SN@{nU5Gm~&Ight z`bt-0mFCLzZTr*=KQ+LAg;a=$dDA`*Xnp|7uHlPa5IuXzXk{8uWBF>s5LXn>)ES~g zv_os@6JRSTz;9N=={(!QZkmkS|L?yCLz)f-ayq<qGfmxD+DTR{e@jYtj(@N;uqpYA zrEfvn&$=_c%5R&O&J(~CuVTMQxd2+(1v<boIx@v>9u>NN^AwHFGc{EoCu`i^Wb(5G z>{pKlc`j+ts9B)|0krnxO<aH+W{|SpBl6!8b>xEnC973N5sFa}*P&e=uPb~K`LeKI zSzfA91`cZ)`o59$e;9MUR^xBUG$(b9K-r-|lekJ{o7(ijUiKN%77h#m8b-{80U#W= zGNZsZj_y1MzyBk2f-8(Sf1qRTJiorEfHEJH)1<14-ztx5t0{>Rc{UNs3l({<%)OF9 zNHhP!P&gyxnr49Do-pAGa{4p-Kq$jA2MGOvCG_{XO4FGZf9x|AD*Qf)RwDP~uvh*$ z`EHXom1Kml7>9w@d$59jLE77%P`(X)Wa_6XAA!y~MZFb~GoPG?5_h3W%_*=EfnsLH ztlv*KO8ly_O`2aDAPNxFgnk7eMzb0Z?KxFi!og#MY+M8ZwqQK#A`$#F9jAU-Qk?C7 zX=yp^+W(K*f957Spxqtu9kxb9k(hy;Z%b4K@Fk=u39>bEHAJEP%>?N=liKkdP<ljv zhfwF%qMXInq<h1=C1G(mG=^{0X#1$bsPJ!bPeOGX9urFYm;YXra^hXkOE<l-S7vNM zIxPrKFHQQ1m2IflqEy?&v-sGf`g%vPSNN?o39gYle?M9e(A&w@*qKDl+yd}?B81v} z9V>gDRGFCfCP?2Z%VxyJ)M^y)@)_V=Q){_7irlmPEYV_vwz-y@bE)3bsr4NX*Zett z&GYEst5`ZLlFlNajUB2=-1dm|hgtXh!w@9dL1STTlhPW!Sf);{Gz%eIv4_@Je24w) z#W_1!f2?TvXaOT51C>R3S|t+Ultf7XnpT1nwt<*BT<w4OT0%umDy2G@Mi*vU*Wx2K zc}H4dT>KXEGDO@-Kl0S`@uv{(O+28VXG?n2nJj}ZWPYv5#<nxEuEvXY%_>33Jge^_ zvK7|q-%eK9eo!jGxAJaY!x!Xbe<c6>(tcPSe^6NgUh&$z0G?zLve4c27^=_)pyI7N zi75bYP&YR;7l2>BJK6jHJ@4H4wT9FHONiY5agVc(wdrLYd@aVXMHP@V<gu`xP>+29 z($Uud2(_b*nc#2sJ>TPV<t`IvAx;n4iCM4#PhoW{^TEz|gOzC+)z_8v5_JS59iy*X zfAc&EnV=3X7?p}~)|K<EYev4>EsH}ijF+#UIm2KYXXvhNWXdG*@ut+pv?$#2ddBaV z;{@CFsp39&rhw!79xidoypRK;7Kw7jtSlc<Oq0f3Rt`g#EHL~fX3>G(s%Y@3Ye%$& z8XEfn872Foqnufz;mt-7sK~ZFoDamve@|qvQY&iG^+D?F!KkHh4Vjr!YNuPpJ2omd z4O4T2zvN~jx+EJ~`?f$4Q!0blj9--6y1Uw8)Ay#-@lZtRw!=vVlhP$U%^>N9aZ*Vl zGgOZbQM!}Q3gwHFh9-?29sV;&DaRqqrc9&q#vdWE$5Ic`1<bZ*VpTw0if~iOe@U25 zt{mIcRwQpN9Y-Eo-~sLe8~7G#xyD7mnR=bBf+)^iqzXDr4wh6x&!5>UKR@~}`_P!_ zx0)OJ>KpBAjzg9-c8qkzIbarDcwT+_n`%4`!Oc#0PjL1pDWTV{G`%E=)Sm)w2_OLB zq%V}-3P&h*{E#~`t*I(@X8-)vf2#h0J-ESmqbBqhyw1m~7MQRyV;i_kwVUjD&_NiN zTrKd)#t5}rXZg$m$9$ZSd_?{L;Ltm<b+Wx+9ZDO{Z{dG)?w7CaC>5?Svh0hgb-=7d z;wFEZco+y6F;|_VY}u{r;v-+(OqLl)be$=@ZqQ7J{i*K-PrCPMxmYYYe~t@c)3j*^ zN3}^+6Iey=u9}ndnEM`cv1FH{r^e+(GlnaYT-(l>Xr5J==oy7=?k9Cx?WJGZT5mf_ z1!I1ar~v%a)smg_TyC<g8_*R-(rx*55y*)hk(2E?@23HK{!0||1%~|~C!<%pe;)jP zi#Y>T&`)BbDA*nbUtuq)fA7H3A3c1<*gC=Ua1txyp}hnZF5!q{3ufMNmp`8HqH(@y zb`IT{t`;aaZu}FbGhM75|0V-~A!7@9@F-&_EfOaRsGlz4Fvbu+bc5`&)ftqWymLps z*kYo|_!7KCmD_Pbr&`{aP4kKyS?Mq10=|9d{&6g)*VO;TfW;o#e-M<KfAKssEY7O@ z$S>>p;J+%tCg|DJ`f=i|R5`C%*!Zk9rdaG-u?5?qJv~iA1a<KHDuepOr~uT5(1Qh9 zV<<oB^C<o(rphLuf=-_on5e*N+94=pZY0ee<HQ+qt>0P4IB>vB2_y4pE8%9LFAZi& zEndDEyl#;#>u|(4f4aH8C7AW|J6m3}L{5?3ALfUFuK43TAif8V9ns^$rV&cUJsM{| z8P#d|WCnb~3ah>n-*C&{qxirSrVg>SHfoT!k-Dy%MJi<MLfab;d4|n|wI3~un%Ftq zlbaXD(7mo`n=IS$8Srkq7D&yB^cK3pHMEneCbE)NK$&&We}6ybZ%4{qrxvphBj%Tn zF(Y{kJ}y-@R^4?=v0Pazh+%DzHJMr``Q0>SQyIbu+P0G;2~u|@8kGLd*s~vAvw5o@ zul0{UdnrNTyK-(?NSM>lg=4pO5{xAN?|hx-5U^+=PxTxFqD%0p=S6D<H)yqv<+ew8 z(zL@}cAsfRf4;YK{G>(BsSSH3uTPI*igXIK7B?;HC{6wMwWPhkFM{NJs=Dbk9Z%*D zH}lJxBU)-!|IO&#Z(r&Cu)M9MH`sZ(RfBj@G$~MjCtYYz(k&qpO>o@!3iKF52s0(I zAIc%+?~yED`Z~B0(t3Jak=9zkXZ_MeUf%{IfQ1kTe~)|vr!=!yn8#ffBZasp_MdHo zk5BmF<&Oedujjly;vm*%H<WD#SLST2)fk8qhXh6Nd4I$E)B7t}i@x}q15|D=;a}tC zS7+N1{du=t8=qM*t7TyrI9XKw8}y?5W!t=PH0@I?^5234vL1Os!b&G#S~B;TXh-FZ zpgqE4f8&!>;6<EIRYH0nH;zFrUu3W%md2C$Rp3E)<qQ}qQc*yI&OVcC?(pcmsIxkT zPFjure50bYu9f`}z0<gs^Xpf)fw$M-XHScdcYi&~5Zv7mDa{l2*1z}StR#2;bAybl zcbkdR6}znI+zP0c99(&mY_FX`UfB@bN3Y|Te^bBqVJ`!C_DsH7QJ)rANvsKVX%!wD z>O^7JI>;g(9>}&_f*8)HgiYh`9TYRdCN1m!ydyWTP(UWT4NzXsmB<pDy6&!x7Bj}l zMkmt^w~yimp2z|z+ogjCi?hdjLF=4<0WdW^|5$~@g8eeX<=0_eNSi>lx4itq#2XOV ze{!0JQDGn&-+uwR@vVBC6X+raE66SS9`$b_Y^R}1Zz~?o5wyBuPzdCa6%@)r9qjKG zTV2nLAD@=z;*y9oF*Eikj!aeYfS!B(ojjL=OX~l_B*&Mn#pf1AVKG5<Rjdk3Q{&Da z(y|Mmvi{6*C0BKHWMYN^?PP&9EXWVIf1MpLri=H1WmBtiXbEY}yXv<mfseSwk>I6j z@hYb4<lGznmVKmqo>^=ZDV<<=`?c#W_k1e%!`L{54X?g_>KE~=g!PXb?h5?y+LKeW z>WqeEsrK)b&>C!0SL?-O=iVi$ma&b!KPTIt#{Y1~-7e~UOh$=5TEB>Vg0mFSf88{S zFFzO625cWS#@0VtDH3Kz51nqQ(KT2%Dj_5+?lq87$N2{OT-ygGVwZe@xv^3<Jp@Sc zdpO_5T-@H$JW-_P()gOna<aC9kT!B(j(p1pu_t`xY<QbSzJ)nF%pxAZ`UM#+LA3G5 z!oS@%{4cV4`Xb%wG2}%Nw5l?Oe?CvzNpKkA&f4~3d8Fkw(1rkDKq$ENF*X~sJyT0d zX?0JvTFN+HrT;y3z&m*{D@1e{N2De#a6q4CLww9i?56xd<b2DtQ53gsMa!@DP82Y* zV)nfonipW1J=XT-ax=|`Gn3--l#N(O-QDS@XPxs#&AvhKaXKc9Qh}oVe`+%G=W)!) z_?%wL^71vMyzpIyP!Z$5shCzwA6U*&^1x@R)t?0eJrk8p6Z!QGP|gv=tYlG-Fv#+P z@7L?LY@b$`<fd3|IV)ma5_&?Gt}eWb0=@J+tVA{%@?PqjwtW)dH}K5~Jo*0LJpohF zS^SLhR=`rLeZz5w-%i{Ie+{p<#tuiT=@vna{JpiqvNRIl8Tz1WoNH%g(6JMU!(HoB zghYl!T0~}fdZPS|)WB%g$G(+r?ksG^j#FhTe}~nUjqS`=kI|HE1y6}`qom5f=*Dn& z>cjDyo|lr0Z6Y7ftSa|bx<f|BSY2&A%y;EObldv<SqIv&`4z+6e{`&Z$05nAg4a3e zL{>Fc*O{o*|0R2j(^LK%HdZ5rK?TY*WbH7vBT8&DKK*~PNEQ?f1m?;Ck!PYo7dk$s z=CX-c%mT3UqPBQz?;nEWe~1KCiA2Y<ID)kQ3&X=?FwCT&Nr~zVBz4N?1ah6KJ#5dS zHx1cPb$$9^hy>Y{fBJ?Q_2dd54`JzfJ<K<1HG1-{<)s`c2bin}V#Y(NFg)JLptVtJ zl0j4!$#;I~3(Bco?d|@6u)P0*t|+3B;afHm;m1%bR(vH@TxOB9Sm681OnRrZepO~x z)qIY9$9P9}F6|fJeCIR|FKN$P$TT9Kf1kpE)VZGf(lq&ce>Klq1}8;5wOP^^3Tu?1 z@a^Ef7WE8|is&`u_DM5}$UV<C!?fS--3-B77cY+I>H)29o44_^`PWaEW4RPB&WD3R zs%{thrytaKZ$)R#QJm|#l(q0?I%N|{{#merR!fDFY9QfYBw8EL_U7Dhd0?z7iQw`H zD6+0Tg;o#Ff56;kM*oAvwRoH5eAIS~!0UCgt=NmSlQfrVY<(Rcb&tsRqC^FkfJaT; z5_G=*Y+9PLtY2oR{E_OsO+Do-ukR>@70E`1&f;&8v^Y1ZM0ALC)GYpImz2zbRQ>U8 zb1<9<-*$VwH}g)Rt-(R0=S<T5Vj`;)@^>#*=tM^ve`C<>f^H7HgbDVK#DmEn1$k;3 zo_F7LVi53D{Y@!uidRG5R{J~ufclY*we~h_o3DLd)elddREV4@2RG3jg;xe&AMfAz zv-K9C1v4ebSeI*^-7N9z#R`|+Br52LNxrmjJ^LXi%p$c*Y$B3lB#5aoNQv=fDuE?m zdUuVMfAantY5Og;D+=(R7W@z!Qgk>#uH|C0pOMX@;2qKmYgUtKBgsU1JNMIMPD=cu zMr<M2GWcv9l^ZE(WAFTvVxDh?)!O+QLwWo1YF$-fl3u~aXn_)gZr*ESO3+m7tDYU^ zq~Dubl`)OpXREnBHBh+fP>z-5J#gI&jV_asfB(Zp<t=9CG=a9rQYd8jIE-IeEu(%* z8EHT1wdq;mh(!UjCc$wAo(FGR$lvoD;iJZyo;Ta=pA&(T*0!&nRESt8JU*BB@`ozN z2l0&Gs8f1=ZFW$6b;9h{&&+l%>H{pOCGhR=py$=!fkM`<E6<ao(FQ``AVX?H7s(kt ze{YvV;Y`5`VF}<+WsWU-|AVP`q0Xg@mV1(feVjj^GRNNcY2g~SDmsCw3lFtbOHC+0 znPVI45x0|hShW90?#btlXchxQeo`>K^bjQ8m7q?w^)Ds{3V=nI?T7rDvr$`hBV>gT z3`g_TBmY=Md~*2B7*Uq~j`&|Ef#eMRe<!99`IL{$DID&|CpZ~NGZ)oN+mt?yuKHvs zm;UFKtf^7pW03hzCpoc1&$orl4~3sN?8eHcHcjKQVBPin&=d#O_!oAdUGQg#025We zu?sgt1~br_<IQ4PzvYwJs0RFFgU$ZA^Rr8vUpw!+JhK9I?XxF}G#ObF_<WY0e~?v_ zd^a8$N>N-37fVNccboYc;U#h(uqWWB;I$GrDmLl(?B;9%zJDxfj<(shlL}Zb@yhE@ zR2l?1QEGaO&qR$eubV6w7)YczRgT4kmKo*M3GDJK0SWnMDTgOSlf$H;Uzd_ugj}4O z^H1)vs1y`_(q!Zt@4iEVMGC0if4NcgFC0X8(Qh0K{iKjm<=4Z|F}~b4cK$-e4F<fn ze?07R(x=)E#Kp_~tA`-_&bSA)b^|rs+bt1vXFT^X{%~9Nu4pwt&n^qZZ9w67DjSZ$ zY%Im}J%S}|7-5Y{RNp_&ah7eR)vquFEsf=FtjOt2=2>~_FzV|KcEs8be?mc$4dVA2 zSV6_l!rM8)07^i$zs7Vy<mzXMc^Ad?;CHlhVizB~S~>S!mD_4<<F1d<=CyF_{QI34 ziMU7Vssq8dmkN>AGQ09-^}Vn^Cun{%*hjFg)iSEVcsD%%x>(ESrO?`m5>7Av6Q4o( zn8?7m7w2vB(si24_&|eaq+Ejv!hfvUTNA%83W!0xEMAHePn?sP^4l0I+@EV$qHt}w zxhPo|<@|gze7oW5!Lj^<B=@F0_%ey4Dff2Py$SJCL_$t(ydmhCsgppU);0KYt7)>% z06E1pb`nadO?rz~xK;=`R5<@j`~4~X$SQIC;L-to;xMk$(+me$Kl?HQmVY#3B4SRR zcn!W&1(MLekx;sFQ=YY$*2>}%rIZ+co3n3jdW|=57I&1-4^GlV`I296yM(yfqdz7~ z2FGu~anMgk7Y&U?Ew@`|O*q|6C(3^4AqDAVoIj_lzid8JPf5#jL<|5~Wx)*-mf)_a zKclV{yZhPF!A)u$9+AOK@PF>>6FR%L<=TiJv>zRU)V%BtoJT9gATB;hhzL-nh&`>d zmr@pTdW8~Ks&GhDOw70Z!lo8+s3U9h@f;CKiA)?@5nEyGPeS(67$BeE*jVm=!D^Tr znV##bpzpy7Kvr)gQFUtgP~q+k<5uQ(9K+sw9Xuup&C|wrsZ#5r%YXjpG<9_rt}e6C zcc^cT=KD^(g}*E<PZ60*(}Vh(oLoG0_PzW;uC~nnO38Wc$*gJ!4lbR~N80fbAmrM= zDF0_R0bAO1SH4=_2ncWu$Sv3#dkpeUi6<aIX0E=JnP<-%A>Rr8_~-@}uAFYuUie1h z_n1lNW6#$K1fIYMYkz@%tRS}7ouiYHnJc{h+WzQZ0O%xnj&CLM2%FJv{s2mQ;58** zPX2y9+1OM>x(mjDQm}6rtXX9Pqr7cE-iz`o$LhyeIFPaZ?Yxmjr6k8wMCpQ;3(ESH zs!0e=FOyQ@(&jIl(c(82f5s&qJ@fR{u%Y2i7M^@dee1{s8h=reV&D%*C&F6d2j|}> zOz?vUi7$#i%7Mi8q&(Wxy~*m&Wb>RBO~VQmE>CvS1M?!R`f-CBYf&rbKEdr5tI-R} zfYe8X#bHF05vwwvJ`7iUse{m2d`s)|b*Y>TWeT~S!KZic?b1ekmtQK&o|;zr>NVk) zZN>P$rSmd<9e;7&c1_gTN7Iz|b_u7hhB6&P6&g9_#wkosZB8T|^?Aurw?UXTRBMut z4|3(ny}8znn@S400ZCW+Zb|R%RWvi%E#DlYEyx4QF1f!siwHLGS|kO^!8afA=Yi{f zI30D15dRpdZ+<qIKVGa8Wd>-k%1ofkSU#wqOyu!XZGT{0YVjQ3RQ~wA%6`wB<#=2C zE~9LC=ERK0A-T`W^%;-C<eNL4UZgnpVR@drCKD|x?g`aTybu3=anta1@B3|^y91X; zyf%*3>loDm*v43eY_~v^ti@4am>GV}__CZ<FalC0aq{ZBUY%pkx7Ce?H^UdWO5#5& zgeT76BY*N(```UwycP{jCNOrs&2BmH5LY=U&gGe=>MlWFazE@{t=~|>*6$a?BzyxF z$&FxlZKK7Z?`v@pIK4n^_4b|#<xN&?O!`pLxRvF5iM|hC7%ZP9+a$#&bs_W=qh3KN z$#w`{EQm9=9Bw#N>#2YV#^rTY<5`3q)v2b&Qh$aE=sGf-r0-a}#l|X<`x4NH-h(^U zAgC$nH|QWA7>!J4xZLVKg~Zw_J4Jm`9*lXOA$&J`EPhqZ?St~4oT#!rKPmYFj95JB zuv#sgc|=gteLbc}rBy77d%4fTT)Haa6YznZH+sWSOo&Wpe(GL@BWBYL>Bpc4&AbB} zI)4%I6|?{S!DKT0*;r~*=qL|3s@~Lr?Y~2p7718kJ@(kt;*y?ekOiHBoA9pBHrPzA z>raB?FGqMP1RSG6vish&l9mt6!r$VavypOp{T((%6cBtWqaAsvRVkD0?0q>@vA@VB zPs}7)fi4gXMg>;FG8H_I!e<tjdubcWY=1#HXdZjwVAuoiHW}@`eTfHFN7*UCj|f>m zY*!MrDFF)GI9%T`3ryj>;F&vBDH8loBMZ;FkuJKksEjkqTA<w=QHxAM)VVBRm5p*- zst?@8tu>ONJXh~kPAlc+sn+mku!bX!IrzRB8v`@51X%3>SB1>Ic@gh~@a2~>uzv`M ze?-J`qr1JfVYRbS8#oKjrd4O3@Wgx~zzJWZt@ltooP&sx`62NGMvkj*mU_pMX4VHG zohVP6<^`X>Br}a4?=I$9r%@t1#;Imy54zGu`0j50u46Z85nU@#7&CYM6*#5<006dG zSY?eU0;Z0RzrS@s@5))#Cv*>`4u7hIN6<isC|HGedKV)WYC<HLZ}y{3Ipdt`Oz4?- zJ+@6+XA@X0(qG+BDqJOR+nGN!Gs~9n8r13mI`rano8fmE)+j(^Z~8C(dDrmRbU{ZB zhkBAG`HOWR#@qE)p5M6TEu-yTugWCX7GL|-YUW*z>)@NedL+YMbA)5_jeqW)?bqul zC=>j9YeTsN9g2j32h+NRt`N1_?g)z->{al(*Lmb$BfuzRo<Ur(XFuO41C6z;H}^I_ z(YuAaC;DtJjBo=SV5{VN0`VesYnAotg2u?^4Yb1#h!&%12L7T&atQdX(QD|!t_5#! z!RgM8I~^3awS`KeQy_svK!4W(nVW;mb`8^fO%&^W4Ehboklvj}wXoxNuhtzdl}<&1 z0VZA60lGr6mq}DA?J3;lO<#+~IAn#9SRcBISl?>3BZO_$1GW`uSfuqmY;)(ndZI8@ zdq!~-#PEpp?Cm0%{2Nnu%T%|zmoI`*sVIIJMSnh@)a<f=f`F=)WPf*RO}C#$|10J( z+lIJILt&l1=2fE-N<vY~Az>D_&<~yBkky8k|33?Ww2a6A6(u9k4eWonNZ6y~KE`$J zxO#^YMbJJ?SYpz;Ha73(!VjCsZ^(|;^Ms<~%f>tzyCm2^FS(&*E%3*&_jCNbO_@e{ z#1pA2!o<pPkp#1SS$|HkdDD?IzN(gCPvfvJifu@@<8Ji%<4^R<Kk;ltF_l%d9#(T> z$}aV%L)ix(-p4PC2H(g6kRf(YLN^&+X!-iD272Uj#*IIDb(cV(N7G@~b5#sD7j?`w zTXATc$KB_(-beO+hx!jW8QE-Tf>;e0HAl1*E6WtXlb5ZRCV!@|xzN8R7PA_Quz{?$ z&hWgcEeL#D9KV}9M8q+y=3CN0dWt{)(V)4R9#4HC99zDHc7^`%iTpLD+56)S+1Y)2 zrI<_|drg4NU@4bXkbjhz+}dM05NHX%MoK3{zxUfApmbqn|JQb|w0lp2J$O;JF5IGl zlV4T1D!?~itbhHZz>hkAorVFc-pdPNR8{dDAd`Vo;|^{o{{u=ks*ojCf_&#g?Y%8) zQE}Y-+sy6+ECWlYYMJ*ZNUQy<#(e|9sN&pNZgY8K|345IO2Kd2;p{CI{hHzGR{0(W zV3D`uFt(;r!boK)UBHYHr|#gbja&d08XY&@;53Q7uz$Gd)#3PZ=f@D{>dH!?vG>Tb zwzkQp<3l^dQ-G7h*TJztRIE5*@H2Mwe<?6Rw=4eof8&`X4YRsmexzL5j}@fV;M>kz z3J8PNy4|>|(9LeAYmfRuEiAuYZ!RlNpP#!l^WvR^<n2RG%$iTaQ~A>Sw_kgL`G9yU zJk`xkihq~Psw<k1!9BnfGEga^ppK&Nv0!$mbxz1T@1)WggICYs=<`vYD?UM<iCY@| zYorkVnCox852d=rb|i<K9^y$YecgY-nY&&vse;>*i<s7)9uEO~0Qyr<dtd2VgfRny z<?0WfL?crc!qy&5_wKljm?B=7dHSGu;c&#qxqmO7RC78DV!7ds^>xP|*%*Q^6@Sm0 zAq@K&GlPC6JrDL*4p^EA{c=FZR~gXI|J{O%ekq&eH7$?`oa%r2Y(U1bgZ68b_tN*Y z<OyssNM>OCRM;V*AYl*Th`2W#E6EzboT!2LmfkTj-L3bBc}XsaRsI1?KcPidPL7s= z*MA*1(@quXmpgSYGe6mf<@|N+0m9En_ns7N@{fpqMzQO`i;;N<NNf-l!-vT}kyu_r zortiTl9Ek<;(>nVIT{`WE;~A^jEE<YrtNEfqt@5et;^$ZVspV}K6~WgoQ!TspYTz~ z`X1NJ=CZ?Yo91+LANDJ(!cDGS>}q^hT7MsyA-{W_zbA@CNJvQPTr^1TifoQ$yzbJ} zyXCbBcT4oggI;?z<ZB0UCp&m8r&f7xgN9G<ptsc8!O2gy;biaiOqnPQT8|>pM8KS} zNEg6EhIsD?NVN=b<%HR;mi$(b9Y{g`EAwg?&3+}fyu^*pgOKz}Og2^?2BgWfAb&RM zn@H!Oc?7RE(_DB`PyPcGB6Cc37rK<U>U4f@>a(;$#h9$r#cJFBL_%?w_-1T!^1%J` z@E%gq!(X@+%xo_mWBlAR5L@<m5{i~2(}6~m%S|R=yCC6%#}m57_@UaIBL~-hTF<Ct zd!g9~(l!y|gz=GYNfOUcuTGhzK7U@>LV&<-8wRGx$FvpnvzODRf7{aqEl8RRC*eVz z7Z8hRH+87NUJKsE2fI}n`J`VzvUcp7DZI*qX@7d3(PZ7z2~BYGNaytQG^(Mnv0H`K z13W3!rYor*bRCZt7}aJ{B@Oo8o3TbdIa9hlxY@684XP!(n^+6pn%c*i%zr<dVKncX z7u%p(mNaQ_ezf;c=367*_QX5Q2krnbXVE#wz`(L??%qU&n_5A2pPf7`hxw4>&38Oq z9VULctfD;k71NFGS__kc>i`e`(rAyXYq|;#_NS>)i#jWxTqO=VrpC4=o2-S;(EF7W zc)flM%-naK0J>|RM-N!UZhwim-h~oH^0b{$7NVoJHrQ@X<{F7DMt;P)7p-e?Z{1(Z zjOsoWz8PGSV?ME)_EpTzDycG!lD>c9alJ6!Th!OwNu1e(-`h0^OQIO+w96ewB;Umc zECBI%*WLU5E6c)d#SlF5Al^D2@!bL1ONGLAbX_qlVL+@mSv{KA0)MyQp^LPtpew-M zhC6i8=C`cS>G;)E=H>4iDXF+U(7ZE31u0u8rj6<hjPjlC>xycTWgAu)q(oqo*Zc#3 zN+F)xpttpwRixw8by7#<sV59g=li7z1;U7*0-s~9XWZ9z=m%wYKI+snA!IDW{46YT z#dsPCy#1sED=IsjI)C%PI!?QPhx^mh<O&nh=Rhby^5nki9{f?Z#PrF>(|}i2#klf> znC($*r!oWnNo|u(JAD7)6s2#mPr-1`uHgYYxsaYjtK)z9$I$j*_a(^TspEzY_dve^ zslonHRq~g_ajtHj!Nfh@fsM5w@r);m3ak>^>1$%J_U1j|Tz^hH%MN4DQ`*iAME>_z zhFldoRvJ~%gg4u{Hf$sUrHyzGEoYaOx;ncq-bZ(WBn#vlT^43}KWmc^h<Q-3$c(uS z3VmAt%FSkrBj!9#I3Ye+%1HOvAe<xGpm!1;P$-y-k|vA{s~l4qeXsl*S$z^aX5L)x z71hItpZV5ISAX@rU%NXhF*hZ?w%@8F)EE2cb41naOnG8!!7a|7$m1;yqd3e)L;H2M z7z~$bnc=`PR?xEKk^o1H{xNL}02wxWdHU$8YNc<!{5~vyIqzc5%*Y5AX_a%&`)#yv zM@BZlY<}5Xs!WqTvLIy8A~8H6_~vN1X??{<szP2t*nh1XM^1FYM&^{#wiNEG({oz6 zDa8uSci1GiU?Xe4>Nc6Z)5Kcuy5uOS@N)4JJe}jm0H03p$=t=UlPZ{E!4)e0vYlyc zrR8ToGeRsHTR?1`9o3l*xycFiYTJ=_^SARV5+Mh>uPspA-^Cn23KIK~vKFUuIZ@r- z8Nqn(0Dn_=ZD~K&oUsyCsFG@8a^Q(f$1t*m0_ybVIjK4*RUQCds8rUx&FL8&Ye^Cw z<8J-O!_jME%ETyKI?aPGGBj@qgL%R))w~%}zHT4JD<rN7=dKqC=WIN;`vFjS-gCd_ z+%;Nz-dQ$`>&#I3`Z1QP3jVT#$e@DR7g4mje}AMFW$(tm6h&en#9`}3bfZ744Fzgk ze#Xtz>p534K=pXu7>=c>PJXzDhaa@ICe#7_Ojt=_Ij$TRoIc5{fNe0ez`WH@U|V1t zCbYf#Fmvi8LMM>zTjYZmCnbgrj-wFZfmF=@;o+>qqUyeQFQOnVT>~f}-3>E<h|-O8 z4u6ew4UCku)F9m;QYtVs3?VhZ5Yi%@L-)|l_5I!F-gEvu&)LseYwzzq`?J>GOQ^hm zk4wGW{*~Cf9J}~q->u93gy7TK(cojm{@3IKXUE6Czj#3RHr3I83m?n6FYU4<z@Z`+ zem!De9GNB^4g^-M2Hmt`#i#Cfc2ey6e}CFSLLSAcxh_$3PA+PX{jIPc*Lh7)T7lps zg*KseH6{D<J;`u4FGaouN-!RjUVbsivd8xV0P;auz&H`YDK$yZoyz))gnk-!R9IPh zBN@C$NPmSX<st}{tY6v{Rd{Z`@s#Y7z~T3)m!k_mP3H9B#$ZT%vrkBmc-y_guYWNn z2LyNp#!&cU<N5h$9vjlb87FE;*-z%APm)Y42jp+wmD$k23n3YQ`N-r~fX6iZoQ;UQ zQ{lI#0eQg{t2Y5H->SD}>*dKB8t4z#rl;O5*hw<)v&EHJKYV6Mq2?nBr;J5GQVwcI zWW~`K0Qute+9kiIAy3#nE-`?RDSzM+?04U^$;fIX@21%#UEyjraNKhsU8244LuM&D z#23$g>t_0Lb(-Z(WnP}EnL~A7-GR(QK)Q3COVXE}6vCworNX`NRL8R&J@8yZ#W+oT zqZqN$SIF7ySJ+1g{q28s_BUl8Y^R&gqJ^a9o2bP@@xry@KCT~Xdrqr~>VJyuIKj=b zVU+(CI5|uLi$jS-Qhw*6GY;K2#XtuG$J>|pHgTzIGp4#9h%16qdtG-*$q4fl=sTcu zrSP3xRio~;Qo|7yG&qwy^aL5|WFE|W-l8Vis=>a<t~f^-z$)5~z<XjHx)*RI@EYwN z)xAV`N<~#+o2w%D+_?vaTz~z^D#0T2`|$dVKI?t=9DVdI%&&viKO>+R{%k_=wYN7{ z&gC*1LB>sOpw7&rpTvh@n9`18(S`BIq~yzv|L*#^l7*|wyfN;1^paPR7H;)^(t%he z{NhqgXyW>leur_L65nd_Xbu9w{M%h{Cq8;(G%5f<G9Esb<QkKJ&VP!q-TtVUtT~Iz zM^F}1c8tcMS4mxxL3np2@uEasn0!V-CC`I(vkbaL<-K<5j3vL8HhOs~Z%ZZ0Na~yZ z%9(UmpYxO%5@a*ok*&iTNO;{FSGD6HQemrvI*qKmnd6LyUDe<Teanu4n=}0FvWjHL zmivMX#g_X6TKFPj1b_EQi);SyCO*W>72W)C|K|*QOL3czs`yOEh2Kq4Bgw>VO@mYx z$=^A4buTqUm35HIR<o<lE*VLlV<^9a7ru++SmuI-^C$N|DPle+k#K2h^(JjHL}P_r zegNpEXiC;4vSAa{GKOY#8Of+QmNTqUc@xEVc2bQXXZM~7zkil(l!lu$vh6Ljj1bxv z7Eer4QX{RE4}&7^w$KSKnEUJ9N~JjmXWKkyfMU^Ay+xMDU>a|#G}2SPF}|-wJw45| z*;74$ZIMUaU;YHAB|N@)!JXMJbmX&$-89eFp_|JXEVnpkmOAv7`KW~G_w1**tPWrR zb3n@zXya&WYJYs$Homy7V-uAtZw{Q$l=$#|C1Is+r4T%kroNJC-!7%%@SZ!KV%Q~3 zwfJ4M$0d7y0P|DnBYS73iZ5d~ye48eFb1oM-J=zj@l`r}MHnF7cwm;Joo&{WK;-c| zKhkvh@}(+$oD+krcE^d{D(7vXF$N;{4WiXoQaW=Lkbglz+4)X++@a_9-2Ck}hxh2% zpv95CE$OU`X>!$e|LLen*+K%*NYR8h9nKAVe;A$OcPBT(f0K;zDEcQiWsUeBl?~ys zJKfKk>QZgQe)#&+7;UhqKLM3k;)3n^l)G*DGs?O!{Wi&V8tC)=>H*+ptPp)=JS&|L zHpHJnL4Wv4sGg%Lt7JidPKMzH{)=x#B9sa=@{?<iP`t6vB%bSiAs&A6-1x|P7fZRD zK`Qy4nFs$Zh!MPxL$cl<8dvuzQI=t3Pv9Aqhq@(xl!q7_4=23p2~y9oYTA24bFoNO z%AMHaa}q8l_L3uKUni-(YcaKsHKO_kD^SO@Q-3nnsQ~~h^QE!pTQ)9gs(LjdWZF45 zm{=g`Z`4MQE#oLzbOE+yui$~V0?h)elm9`uUf$VwPEFi5D9#iRUsra>TAJR>mgT2; zR+w8g;N$RkPD99lH71MAJsjyd>JvP(`~%nO6H+#w@p;ZECT_$ZCeNH>!HYfTmt>*^ z2!ETq_@%s0pM0Eeh5~btM-_g(#xu__h0V|2*emrRE8JD7&inKSyZ15x2eOU?>8XQM zQIeE|i&i(gnf7^qwNo^0gl!sUUT(_<@_|m~Yr6<Kv)v8M)-iH!*maqpEdOBj8wQt` zE=-)iuPhrhJ^fihlsD!)%zUb`a3}L^j(;&}Gq)i7tjpt5#OE;4WD>=Xqxa2$s(|c5 z=|^iHPCD)Kolhk)r(_u4=jZHsBv}hPG<Nu>R~TAF9#pJQ2JxA-(FEP?HV*O8<S!T0 z1^SK#y>56Iz3v!tbs><w?YuzVW8WbC4+EcTe79&n9vEZ{P`UEjgnULVFf>Y+a)0V& z2r^Ib0izjeq{BC!p%_<$0qlS_K>#cdM5(X3b^dihv@R~<3A%G0HnXx9@edpwg6XIU z9#<K0A)WD#U88-Q(P`uSkOM;Oal;*>IuXKoMsF?khtMaK7C}w2w?j*}qfewnS^hlx zI-~bbeAaagfM%|t=5a$UmYqQC#($x_!onC#!$|vWZ2Rb{_6cC79rPk(GKwjH;=e4e zG6zItZ?@h99Y|q^g&N8cG-e4ooK-+_OYwzn|5eZu_0<b_f(6Kamyi@@-=OqtYgZs2 zsHKqKE|gu&BvfgYmk6f~(C7noZam9M7UX;JBu`NHjw*z}>kqC#unU}1)ql&oa%%57 zvbl4zrHyv?w!Wz(0Pr`(<ChcBkE0W%4?($qhd2aO>aYU@6;Y%Xb;K0l8f0N~8G#GH zE>3?b8db|XEn+0Pk`zv;w%RnD`Hn8ujJoJW5>_NdCLT_lSVXmTVnpJF*^2EfGbdbN zg$J&Zu3@H6yrP&FS;j@7K!1lWcteMa{r;3QivPE@B$+8e)wR1GJ1JvISbGYj8Ze>G zE1ADRIz_|JN7Wi<lojTr=I<@pK_2p$>sHB&do1<Z-B1R;oLrXx$Rd~JEfieR7O9ML zvUXsM_^#oF^UMuf!^_RLa5ieT0BL2y>&EoV>Ud7^$G%p&psVu;r+?W5#MQ+Jp;rn; z-`Fhd-^!dR&kBY4t}$i3=d@ru0)P#xzUuCr*SaWXtXKBg${N%w(T*oZ>=8wU{@t$< zl@DmYZqez^{BUAA)@F@5AC)nfiW}5WZV}OQAlM6vFV*~b^L4LI-3@!@Jh36yI54Sh znpbu<*!Y$^gEtplI)9SEOJ8TRTzRmJPNDbgO7}>;RIJt9z$Ao|P@%qXd>6adjwR7C z_?aL$@!zLiuiC^X*8!>2Ykh3uScXr}{}8Fn9jnsT2LE?%_<uV~oK4w@D%HpDryYW` zi;AlQZ_Ya^+<u_9(0^StGhTp47Jn7UD^6^v3z}H|lK!<a-+$z_($N_vL@3ka9HhLv zeZ)F*d;GBrj(+8+T7cQ~Vhlf-&Uq(XZl+kqSR^leejA|*ZiB`1@0h)4MLd;;T`d}* z0|d<fcI?j_x$%K%Qy6|CA7uUA&dn}7)}U;7WKUi{XMOgm1(EzsDJJRrB$r7VkDF~% z^B`YKbMxkJ*?&>~I}wr-eYX?P=I@EqoC#t3*~#_lL0i2ho-$SwJ|m!Q^lIBDdc-Fn z-j^5pNhf!27ZKuzB<=nl)rrjUtRBJb77Bcc|E|n`H+xIhfL=m|sq_!)kLh)vy-G|J zM~iI0<srWW_{D1&7G-_!b2wS^7OzrbG)x43l)w7|7JvRaqQMsbaVY<Yy=Z=)36Nq| zwgcfG$&q%772`;&YCp4uaOk;v)xGf2j~T_D?N<h(GNfIg+0KzfyKmB`WxwTxXKB+~ zlM2h0Hfv-u1JLJCp+{M*HwXSJYd>^Xt~T?DjOMH}*BHN#n1o@^CQftjeBDyf=$|$e zhCR@7SAQ*sS`oBu%%OfAm0nzdXuSm8z77p1|F#2kDm+@nY+9$^V*39iSU9pQO9_|e zWa@U6DgDNaFUAirp99@%x@q8nfkNVavZks}o>mWk_*8kz@@ytV^xL?iT%YgzXQUtk zYjGS8Euj~W7GBcqt|dIu0AHe&=D^O9X#t|(9e;swW=4S_VV5bx7?KsXFm7m=i(2ic zvST`OgC1)lE?&h~rk#nW9+74E&>Ia&v)C%-aX>&H(!rbq7djzHL|_k{X+bBCQv$3Q zx{o!f;iEY$SP14|HNm07EyKrG%W;gohU+3Iru6mofdF&tV2L4AKhrR9;@!!-m=VDc z<$s2Gy|P~dMA4vEn+CS&xtEw-d$c>sdX?mA6n6kOiwcs9jKElL{@qz$CtrEf!5JJk zpBP`$YiFVt1?OmEy+3`cEo)4?QLkJ{VoA8+;#pi*Q$xJw<e03`_y%b4HTDtBXblk7 z-+7it4{l(in7*^DigoS?qLT=V>VJ$UJbzwg))ZQL%+e#en%uYNadf4YU6iMIf-Az1 z4rq;XA%oh0@bYQ+g*bo85liuJ9x3kmXNW{p6$Mt;yEtea*eNTIS~h$-x?o^h2Oqa1 zG7ta#4{MYkWFJl{krSG-lzD+0nYSGm{TGu47R2qBJ-Zj27hE&U!FrpE;`(@!;D6`9 zpT^Yn8QPvy<1~sx*3_vK()J@3&4&%``g~?&INpF9RSK=XuSJup?+Kq7Tr%M@3HMrJ zQe-|D`8!FJ%Y8^7oGk44k=)ut<aj~qnLg*ozonqO_fCj0taq9e{U(V(C}pV#eY2p` zoJ&pge0Yy|Z(>Y^f>>Rh2!bIns(-~MBBW{%iK3jXem@j=W3u!g0TqlJWRZL}nXXY% zd@dewvGmY4E1>8u2uZk^8F?7H=3y%veLfT3b+cWXPobIW+*>ehm45CuB5mfuzEGMU zWQR(4SZMdJ;5op4QxfCgNq=52oZO%sVpbyI*PyM!4H*Sz6R)S9?MA$Nc7Hk7`<Qa= z955sH^Z-q$FdRo2z?j%&*qsBvichz-)1X}^IeiUNV;@>!I95W1|GD{{qr>v;H)!!@ zD9T=gc60P7vn5LBDuRt6>Jw5s@qqfJHS)830C~9zOMH6Qe13rG*N?3#0tW9%QAjl^ zgYf}w^!rs6#)FRZ{&H{AjenA#fJui<1_u160G}|XQ;~pEH6ws5|AY7P2Z_&`dyeh( zCyx-oq0h#VPC78>jBO4@(X520s!-^A$&y6bzCp+2arl3=RmMGwwv$LeDN_v}LJIbN zPP~2ml;{xA@Rw@g)1(uyV!Hm%pXZb|VKuL&p~rPFt=|(|@6K3=(|<-;gB1=OlwaG% zVz(nWUMF#gxuc7@8P*qObaadN`Bv7Qv!JbeU@eG8oC8$Re@n{Wyf<4jQ(Ximzxtgd zuAA@gTJnN;I>Er(<jVo3r?fSlbM3NanAf6)jKLCc==vudMD)?{`_&zZk69DZ+|Xhd z)JeI*)Q2iimS!4!Ie%j+4Ha94mMc^E(ZScF0%K7v@%v8k0+p|PI1+SDAe=IknD1i3 zRGU4|=VSm>sd~kb3}nkqUJq|&l`j8fo$Qbq7Z98YD;~R7JrgNdji+@?<P+?42gM}- z-f4I5f6@+x(FV?9*KHZ16Xg|=8}Yl}%K6tE5GrEwOnywo`hUN!f2cQmulyr%68k#q zA9`Y?RaX%_6;5@;zbSsj*kBZFxRV$zM>ACwiJ5lseRqm(bxkCgq3X>%9b>Rfak0$R zZ<-Lm=C)mzq)yIC(L+t)4vn8848?NOH#|lWfZ^s3&%a$OUFrUj3|`2pfBT@*lH7HY ztSM8UP1^`+Fn_w+$vKKQFc-_oU%V>XGv3*-8hbt$G<vi(X1&tbeiwK-*nUf(-iP*3 zS`)pq{KzB9dpcwy?vvvg>}LeI&?z>e+mH)r52~H&eY4DXbK6ILwu(9{Gl*%l!QJns z-G3R^>Tt`m{u8+A)OK=BbBmXJE_kpR*r{eADH^!W(SK~$8{Xle&om)ne^ed4`A&M` zAWqh9BSjxqgJtJ2Zv1ht>ZIjj(DAGpZ<V8CvTYU?x3M2bs)b{Zwpv^6*7gJ#gt>4F z9w7jl326HAnCj+$%3s3sPl17&Xf;JzY5w36C?a2$J*=!$)%gBkTfHbwSR?@?OFAoe zf$2Sw0Dt8&)o4boNBGDlpuxWwQL!V27{I}mZNB5Z5zW#;#v{|Qc%w!fsd}k4P~Fz| zmjet9;)S<eW1YXp)WLhsR#T74w0~4^YdUe|`_urf6fGi{E{_#aVJF8&GLzmFX(gLZ z;>nJ7-GU1Gfr1c%#y6@Y*5}e9j@I>G`sqcAkbkzX3(3By3-Wl(Lj!_WWhre>Pj8i* z@ACp5Bb$QH1G$cVyi)J_Gq4E-FC3bu7V4YoUKb-m+^%!mN<C^;bZ9)>FNqw#Eq#Nu zXrahX07!fYUk!aA=pTa8e0UgcY)^gLLNBxQ3wXO@;1*78?x%rbZ)B^wxoG~kMEh^+ z;(q|-eHg5WchX~MA9Awb2We8rJvY~!NNgx=`l{T}?%i6g*>b8ymdLKjx4PBlJ(F_5 zcpd~=85~AN$(O=P;7f>^J%xhLqQniVMs-{KP)P$%ubCvj+7}H}+RML=R>{Fiz{l>3 znw>q1c6;BO22_DBh-M#)D^ei`m}ua2Eq`p0_AmP;wx@{mG-c-a=cyudg_0`MC_lQM zhu4;l7|qFD34Rwyx6yRAtdceT6TwRodK~+X9}?KmXJ@N=@}@BC0n~h@9bCUzjYjQ> zDZzM~R|z`Sxzp>lAZ94q6W@SEs-Z;wp&cd<d1Tlun40?YZ#34eWW}{~#Z&%XW`ABC zKGZ@)MtPHyqMwlsdW+X(7&1Y9neeU4%{>QWQnJ^Pvk6EEDm|R3>Tn+(VHCSzRV%Jy zTtfR9Y6|m@^ml;Zo9TfiSDT$We&>NdJzm|341oe_g6`E#P5<koZ9C+Z_{!aa`Kd0i z`74_HJPkUw9@J8;=E&6DgtwpOM1MAM#^&yb2+73>yTE^MXo|!0#7D9m%ri)yFD}Uc zxc%n-yqisK^_|G2x#0cUt@!Edj#ZR}pgC3=*(8{eiYC~xHM&D-3jMO4d-bO~CHT#q z=2L^M;;vbiw%(77nt{*VA~zC!3y^*eo1lE^N-{GHiu6LWLS5<?ov6hAM1Rrh1V3@O z`mVv?=R@WhIK5a{zxMI#uM*k^$)vUoH<gLsoZl&)p<gIW<K0&oGo`9&-%G+-HxHp_ zrg$4*=ygjJ1GoBew;Z2vz^lbC`aN!gV)!jjsua;E?`KijrRbtfpIe+cAfRPsLq*&i z_<cc4hW6|Trg|=^ZOXfV`+we!9_tL7_x{f;tL?~fx1Zmmtd-?SJI=PhTMEq*>q3V$ zOb$SfY=voiK>oy-87HR-ab7-U-p6Ckg`h^}+xv^d9mAP-K(-#t&zo&Aw^>uf558&j zRN03seq=)MEw=jmnO}66wyDc%@^A)Wa|7K(S+Tku+e1;GKQ}w6s(;8IwbVs^G+f76 zx7|<fvtY!o#_b9*S2x0c6iDBg1zfJ6R+jGrS3EG9ii#!(mYEo7l|6=`)Bv-eg|L&2 zKf4)MpRQSH2dEES7OP&GPTlkiZx)YQ;l7T}n>okMP3+vxn6*)r%PTaEb@`oNjKPHn zq$^<*%9`ERRS<01j(=|LcR*=>Bzc0{7-n$yTV=}2u1a-aAho7-e#YN@n<aw^Q`)>_ zq1|7yt^BrDg$oB6>Rws{Tmv8V#U{=DR%WUk(QE`YhaEDz*(13?roV8bifr*hD<S%P z-RE&EGWVCdEyc;XHvGR%?cZVF&Ky~syX`#qO&M|4uf8MX(0^IUCdD|`Iv{L3=1G^z zs{ibhQZ;?!5jRgRKg+e`3GEq-lGvc(=V#$rQQ|+g0@xoN41)y<M;ZYe&>{iN5z7cD z`gcxE4qT%UGdho^*#4<<bWQVA_iQEVr1?|;3bg}MRX@-7T|S)yNtfRMgKfYa4Em|E z6t2w!n9^|W(|=xVyPcn0Lp@B;^iQA^oWsg3CDJEg@kgHVc36q?oke@i%dOI3tkn99 zI0bldG=<=-FO-&yixaOj$5S2Zy%+LJkr^F{?8G1?CfPXkU2W1SZykq30?aP}6rppk zg0Nb`%BbO4MQ`i(Ob&kkY)+h@2>nnX={-0u8q4h{8Gp!Nz|S(b<1zik-BdS=oO-h= zdK_@ArGP?`O=^nX_oX4xN2kpy9zK40rsP@jVH8XQJJbwoNc>h)>>&o~JkFts+fS14 zwSAD~;gM&k6Jt5m{R{In*`H%M4Z6`Iv6u6kV*M}b3*75(uVT~HJx=;!ivJ$C&F?fa zsHR{A+<z&2SFOQrc-^QVM&?F0e?}jewdc@<^UKR2Z{2oh8u~Ipmd5=fZH?y_ghXY+ zfQychvYc`(rNzBp>ChLk;G@VB+-(sh;!jgw_QDbgf;oEo2!aRXBwoPUWquFU^=AeN z4Q>w8gK|e!7}M=9fwJ(?A_kVl)SN#}kct0tOMjwR!%GZlOulIp%28eTGmo6B=1dX~ z#zaTli0Q@P>D5=NkZjXhXEd)X?+o6t`^9neOV|6ql@`>Ik=_9<CmC$V^bv3H4nYAD zi`zI|C(Gn#^fYwO6cfC0_IP-WC7|msJdZDb2p1FzTxnpeguJeH^pb`9xN|sP6@5m0 zr+=rayg1*<*s)5sc5L-OGiNWJoE3HIia0oMH}g9P@N9v_@~0$T^;RjJHyAe^6;%mi zMpxvUIhOtWc7CuIvF<)2s;LOa1nogQ>e8R$r5E&BuKncwrezn1eG(MPek{t+GVUt! ztww;)-_f9(0RR}^Zwp*W2%dKVJ=CYSQ-9@6VqVV<>(fRrUHySoIe3^%e|JM&n2)(L zUukLF`|e1f(|C2BZ`M7aQ3+AzGd1%ywz^w?Blw((Ic!mD*=}u=a8-|wwUr}#fqF>{ zM^~L;iZ&+Bg`8o;J0(LEv(i&9*2luu5DqJ=sbwpFGF>?{cICsIk=4m_y^x*um45^u zW5S{eB1gHsIM*Ly*B=U{$a+!Od&IG~{p-Nf2RiQ0?K$=Z(Ft<Pn(5o;#IoG5cr(_c z6U*SAXoJ$Fx7PzH8%^joKo&W!;;%n%ypFsWzlZ-D(!XzfHghv2U!{B|mXc!<edT1L zA^er}e0hj-J7Mn6X7NG6`yjJF6@MaP$~s{k(6B<XU&lwAK#dqHlsf7+D}5Bkj=bpO zh0A<3>9?vF0i5n<wx-oA{N*KyRH0cQ9U5?dvY*J`@o>IQJ;x*d9!fDDZd`<l!*WLy zCCFC4+n&c1?-0Wg<@=lyzLwuZDS>g1)Wk7x<IwZaYnE{MqUiNi=JRiE+<z9ma<ncK zZ6lE;+at=03zdYU3z|-%CfG;}x+F-}R(1rA3aoRV4gHF9lpMHX`9ZRRtr6)oz=SuX zjBDz#jGZrkLIx)bczW-iG4Z09TSVP)_=5*yaLSk|Vx?RLU{EX%%9+TyP`Q=EC$Y*d zV%+sEPqZeQ&*@2w5amvrJAc2!+9xg`qbVOdSPoYH?EVgW*;bV*n<s=#cN^~TF3|mp zHF<OO@D>wPGGPSuA^8aRtY?gR)4-2oJt4)HBMS5BlH~rY%kzZmsOJqdU&Y5?0hT)A z8qd@@R;yXCfI`^Kl=%L@OR!Srq&Xz$>vI<6@hEsV59S|J*EI`eJ%2bh^jF(Lf@J&O z5e`X>*VRb}D@INWTPEPr%N*_X#?#}g@VuyR3cR_PtL993ZiL#moS$vhYHV%0Iq-+f zJ6^@u4sh3h%#0VkTl&yb;w|N|niA)pFD5^4KaxA)lB(XkEyg(Xn83AVu6|dyHrjjV zefpWa?vD=ol*h#MDt{%n<CwAjW+y4To`b@OW=YZrt-ogN9`cP;#Muj6$+~e`taBSq zs6!%NA$NL**p!rui3;OiTmb2SZdZM0vp*j^zX)>isq`Az8MwX$ez@Q3p^p8oW1eC& zPZ#}5#Rl^QLjO|J>@WHBdefl8;lyOwyNp4%wU#q+k6ZA7;D1VIkHFcr6RD=#bQPa- z`kPYom~{or&>PMz1Zj<8BU|!CS18sgbCSh4H6#GyliQGM0o~7-v*h3Q+cV`|-dp^o z*yyBSyWgK&+9lq87PCNT;$pwqdL3D1H}tbAXyIDG;(VdGx%q9&8Tx&l;_AZAA;IDO z%H1f7xkX-6Ab&)ve-P8dPYl4@^v=XOU(TvuMj`%eS{>0QPZc*me}qc3N*VAHaV==S z5d6@r)jQ2{K*!B{giPP2c@ngqfcaD`EF8q^mj3N!7&{ZcdR?WOqlQ`9n5pL@iL-d_ zf#>~%U5^VFW2$0=!bv}@OCk-?Q~%T`JU}*-I3rP(K!4|;G0lIHLnbr4cz&tH)!uAN z{|N^<5Tcp4RrEtC-x3UrRETFHXNl5|b_GV_Aj4%Mz!s6wlmhuxiEKooCH?#2m;kAJ zVtGW3@l(Rixb{sm0{GSXXYpHYVjTWFHceA?1Q%fCF2VCXauU33eMQNJXr0o589NMr zDxe#h*?%C~Z>pKb^pB_FxwoDPAHR;__CSS|_3$1Md-I}6iuH&LR*wyOnb0MYk=D8C z%KVTQMXHj^EdlqRt5ifPe>1&WlzZ}#wiS@-z_|NXCF(Ie-1ljppQFZQ3fxvC`1KsC zP*@=g;ZbIh`R;Z-i+{Svm*glmjYP<3&hf;<{C{}I(N9<~j|G`qG?|0Sd__o*nQE|+ zxcXmujb2?n2O0frZ*v=~_tCOCGfAmTC3`ybXf}4XbATH-6gldXcAE+KPD<N8(ZJ${ zMXC1=NMXUz(nmi_L`<msvc$}V?!CutVkafJ^y<};S~egb5I{4>8vmmPpmyyYA)H5k zNPpyXL9i2g=A@rh&JtffclLt<$aFXk%ZDB9l<})#goAoei7blhSM@rmQZC88(25Dy z#NlD|KQfm|AG<2fx=c$^OI=ludWcSD?`rR3G*TVxI+}G_!rK)u7mmP5e!#{h9xTyr zPtoQ^9`Hk=E*5WI1EV96Q*GzWL&hzEGJhncA|CeC^uRv0U9F&MwB6*-R6C$c&9;Zh ziY@P3OXuu+5uZRE&!F=wM)ihGrM#&*)<@=YpS(=zjh48d8Y<o}(*sL94%cr0E8+(D z*si#1tKp0E(n$|qK5yaL8N))h`>0hTzW0WRs(@60#CnOFn^sPllv(E+Tzs6v!+&4x z=k{mS{ae-CcZFx9DqdfMmB^Roz=<j52qa2#M`Av+>OF8vpJ!hnfavJegB*0qw~T*J z)|~@%K3yorrmoGH#Jq_lk*R^=H1KY>!v<tb+zyHMz2|<M3mcDR^=>k(zaY-`pO1tt zcgUF;y6fX#oE_VW2sQu%&%H0Vkbh<c?GI@TKxua^#~exeX8s)US2xp~YCLb-sVZPO zdt~v~`#Ry*I176`3YMU6t_yyHmv8nR9ZlvJ2Bz0<d$?znrwqY-8$xE;B;jcn7oZ8j zXWRChR;EYGx6a3H0)I;m=|^(FC6zo(_Ih;WZ`h-3QQ@P(KOJQLUi#Q+Tz|Qba3;Pq zTA#WTd@CR*Y2?Lo=nb81u_~dn(WRom5oj$Hkn*D|)QlSk<{+3SDJ+tPsWi9Ah!thJ zk))B5v<lQ7Fp@(tcdm{jR3g<OmBtdMj4i^>aviaAE)0D0e4?Dvz#V6|4zL!jI;h=U z`A9gVSb*>AGYc=t6a`}i*MIcE{{qIsm4L^FrFIf)%lLfS7|KCXk9U$Z6FC5Q@+QgA z4;8PMhA=0PiX3RMMg+AqP?Y~qSFMEtoIK-{G6`om`9P8)5hn7f@O1l`j+ez(4yLgI zB8;X5>GvH8LehOM<AZ`0bJvbPa@j|wcOc1Z>TURkBK9wOt5MlCSbq(h?)KBI2c-K_ z8Cfh}#6_oVmQmeI_TIdNVwa3=*>U@#2ffS{8468+BGZmmmt=8?7q4b0Lp(P0X%W)C zA<;AHbKokw$>S~}OS<n|tiYyWZ;K>p3Qol7@Y`7Hf+Nc9q_nCjixkp6eEp(6C*t#u z-T`<`JZ=Qo8WN?Fj(^Ka{FQ)9G|db2XYmO(radtu)qL|_rd3kNqoKj+V4+BkTYzj( z7Yqg_ary?=j{I{ose$s`BkUBor}jV35WLL==05{6E(z+_afXq^6Gn;$-Br#e3Ky{$ zkA(lpxacLK#}1x?TM)JDXVf;otI||#5_o(+Aa4FQ+_!Cysed7Af37i-zipcSUcF^n zD@gj`c0z}WH&XznR;2aY(mO!tpd<y$e%MT89SBUUX=ot^fGiuP{*fj|fFqaVZ2i2V z11qY$oeQYH$7nJuRv$})=B4-P{MLbo*?h&>G0m_UC`3NS$lE8P60I}j@!L1rIoyMX zsfV#=t>1>3!GHRNtXZ2K7;0@Ws2g1%@Rm4#a;%|tlkW?n|E?l?YH(xXPI}QKo-frh zLl99Y^O>Gr*ZMr%ZOWxLyU{f@a3r<)%(HLy(OK2UGnUk(vaJyh<+Tp_c4+N$wqpyr z6*~(zzlXa77vJ<7UB%Nf4p95uzJaZ^7Yt$>M5Uq3>3?&v%COI6La)zbvC_$Y3SxVD ziIcj$Hsi_(=bL#<jUSE0Yg21oR_~L@%QazO!c=zc%Lv9eSy~zb*Cre4zW5+&Rb$#h zS5*Zr_v8TWUFf)a+Ka0jv%nMB38Y^fykqA`oZRu59_*2~Cb0S#nTT`965)4=aS`GB zC#+*EDu2w_r5X^rKw3#<s@od9D-owhOi<cv6NGmCwr+WA%oR1D`&DpzBTfEZtDFqJ z42xrN4WKql#XS-6B}a)COCr){UHIU5B!0BeYv5%ZSClK5A5||xT&m&bq&>m_C1XPQ z4=`DGZk;e6c6%NjY)c%*MLP{_uHqi^jDe1$(0__^7pFCWYDN#b%s*C;Bl16Pt%{j8 z!xa%zyZk=&9WS_m8akVpW6N;t;e4Xj@z3uvXkz4yhzTqEmv`3Bi^|aWc_K-O<bL#} z*PdthaDNJvQCV}uOKAQ3Tdx`{F&&!G<AW<q`CEf9#~R~|P3r11#6*abOmNBsXej?< z2Y)=0g9CKp5x@RLAmb6g0k<_H9A?5u`1rAS0N2rRSj6rxx8y&Hg>3K6h|cu$|2Rf( zXwyE;-5gjXiJ51dc?vG20+N&rOb(?|BoS(<8z-R?nO>Qy3XQWx3jSb@fx3|8#oWG> zuhW_KDzb}+6A0_SFGL}IQd2cja-Vi&Nq^CaJe$BTxEFO;t=AS=ZeWKjn>K%3I5O~M z0Lm2$nE(MB-)q^K*vRx{R(dqZw>0brGz4a|%_9cGUg6r$uKn6)?_ubzJQY78p9t9y zq<A9o$c?2{vm5;ER{!DkkaqrbwR|KJ*rmu6_zkUTj}&jijE6&VCUXiN*Tn92bblfn zko)yhZNLwnhmQ4p#(<#H!gd~w=B1Cj4Ew_Xfpu!IfKtoJkJ88DiU+f2sH4AghvEAB zAJAznD!pye`>pKKoIXBfhiS79xucb>4mStm)m5=P7uLt3n)tD`HHl|E_bjtlH7be+ zbah7^f;)1$--(n%>fDxsbRE7rJAaQSf+&vd>(P70BSRfT5j^G{*lU9xaFf~iYg_lZ zV&@fppT3ooX4?caFhHI`6*CQQcAUg?v!Gg>u|fq2)^oNb=583bA_CAj@~+3$id7m7 zQK>Zv{k;7~Q?GzVgMWN&_?i16Tk@CorRd_j5(izy;1(vWSl+K&Yc9T|B!5UXOPB<^ z?CefUPFIR8vA5-Zrf;fH_4a;}4)MByK{kGvt$g(H-^gx<&cWi|%v~8fTM5n1HVe!r zb)6Tdw;BJ1wEwiO<hD)13d$&7buy&h^>!+y+Q*brH7t`~o969F*<2J%$qJ=i`{K%W z;~hAgaF~ua^g;T0-`{jWZ-1E0caBhjnb#_tE(6x^qwZh|p2#YTDEZ2yC$U(}JmW$H zVT%Y5o!ZlKL)hn-AWlVqYJ6l*^W&v~_1FES)kY<JrPv0V<bOI-t?TcJVTfuEy}*>( zT#SSVyZi~yKEPo7cUO&w?|gyaP~T=s-5C!&yk1Cyb4YccFV^YIGk<P~X*3n81G!gi z9*q<-*a>!KA;Dw)4@I?QxF|!54i7d*DD1G#lDle-j4fwC6C%f!d2Uo2&?{ke)ZOj2 zW6=l?&)xaOW9YzuX=v&M-_S$QvdaCUpR-VzoBKLR2}<%{+n>sS!0@ua3)Ch%;XOZ+ zpQrJ^5c$MK%Em?|F@Lr!EFavW#5{<z+Y#`_?GU|ei$dmik2L>lSYs(VJ?O?i4dRFZ z8RBa$NlOQp2rPPV^4ypa@Q^Siq~q4Aoh3&=0l_5*M82$a{Ap%HaVUtOu-WW;E!pDt z=ub3O1M|%LRn3q)1sZ)+r$$8i;A(PE>0$R^kiRiawJkVK<9~~QSCScx?_sarTehq- zy1hL~?>{N#d#hbE!rUG4p}CU2oH?MtVT!pSsp0ydeHG4*jfjS>Y5<Pv^ds6Wray3O zLUixg=P8DoPE0LB^3%0%tK4LDqjfPW<3doWyvX!;d#VlaZDt8N=w59CbMZgBCcGyu zcx0?Qb^NXm{(m_$0s~V8&m0<Cl6R&C)L=mT|JR@pTQjP(e-5~|VIR9rEvwrtyB>{Z zL6GNmNWsdAygNUg`OP2O4kAhJkC&oIWNrm+_w3uBEd}rU7YxA&)C-QDzcHvZ!wbGd z;zrI+iF%kfSvePj&ZE<m5*H@?7hv+9DFS8FR`$OQ%zynlAwz-ZGp=*}k=5tyb+tdU z_6vF973vC~n9o$8KTqo%OR<_>YtY;a`2B{H{Wk56M)ELRjaR$xhtf9rumE&EavZzu zoXbZG3EvM3itaS2qA-3|&-2W90(H!tXBwn=KE#Vpyev#nHXNsFuSEpWw#|r!C!sU# zLm`-7j(=i1t1h{z)h+JZG5c-tVxs}8CdS2@+k1yI_U1d`;2MR8Ms3Ad?tO@z_*klw zv|a4p;P>0+fc-4aLz^5fkq^FXjimXL@b&fCeIIRVlALkgEV@1A>e+HLKZCBAifkW$ z=i2>KbKV<gat{M<6RT`z>p;G?ha;qBm&L=G#eWP;&2=<9m?_XZ9<y7RE$*nefn%#} z#8+<8G7MvBKRb6r=HJZ_{N<zDh<8!S4_YGnod4G))s^7zXD|Jbj-{B$UlaP8h4r8I zvrIm)6W?Gu{_iA;a$?_Q@&<p?5uloL(?1(*|00V!MPZjw{5?6dH<yTl=E=ZMsz4s$ zCx2D<`yQ9Wy=6~WoOeKZrTdy)@54y>d>lcGwM#IPF#&b$YrdhL2@q6sLJ53uI8tpH zu4+b?aIx2j*5?X-PjRenn6E39Q#T=UsR0fs9R@R^^22}Nu$a;SJ3z$0iQVMgoqK8p zIKXuA#S(JLp5}muIPs8=pY!GFW<>@4T+t=M;ro9vxSDpyw_@BBSF{N|&QoW(!-1W6 zPx$HEt!xT7B>7A#y3dS~^fi)y-%HDU{1VVRk@2=G-Pcz`#8Lo%c*Q6qn7nsMIxjfk zDK>1d_vG@PHLl%0pD|tF02GJ3rAM?UP%D_0W{Weo{U_l6a(>d9U5LT1{~npNzKMEe zk??=v$XPleMp2!Op<EGS3P=kq*|q`yYk_=)1QoJb5h6z~AUvvIBw*1fu)t`idO7+% zHIIk!UR~-wFR%nbM!X3IY%C>*kcubB{$l{e1~|wfI5fd6A--Ax9rN(k;Q;!4Y3XEu z1d#sUY(dt$RI8bwO5@Ae3$Ch-%^R!ooDqN43~Q%1COVkf*ND%0tMOOR$nbmWhH4h5 z$YpwB2x-rgrQ66+A;1`OCj<dLk7A+Z$a5v3mfB20q}F`Um#DCQ*H|yrs-##@bv5}7 zonTORL$JQgKx+YS8H8oGbe%QNEI6i1SHaGE#I()+F2X-Hlk=4Uq<=NmnK)0!bcBDC zOCePy+*zcL!0re40qHx|r!PPxc6dAN1YTKJ$?pH`{AjAlO|&bJAWTc+!JRrbk?WYO z1v<ZI65$uoflZq?y9<g;DL&=)e$|Rvp-1u2rEr9qS_$GsjmMyM$UXRj*11C%*r&&? zZKmKy1Kbwr6(!Z|aO+JU1C`2)E=PX`i+R@UHzP79d(+xNFE^ah|A07~Xv_6ImsM!R z+!kqw2h68;D<4$Ek{iMmndSh-8woDtaSOa^=i<(W)Qn?2k2p#Ke3*I~rznHeyd$HE z5nLQq0<DXc%`JYI4-Qir$!rYOU6C}6XEnf|isu;umhIj%WWXAYk84pcA!vWx{yG8! zw{8e~-X}N2R=9OQ_TLIV7&mw7UC{N##f7S4W8aQF4`_M_JHFu6@yyaM>C0sATj`rq z$R?C>E(PSOJcF4rpBb2~(?G2#PDzU8Hy3}(wm|Z!X+D^P-1=dZ)U)2IW1>aps+w&s zb7FIX)BF+qFa|FU6?#1!2j+isd;^M73&k7r0L)dD>%J6$_+oysYFb3`lA+z${lQ@{ z-D1!U17;6eTOS4L*pg|HU-Coq0)uY5VeCKcIt4f6bvhmzAI_J2CN{nuZKd&+{Ba>$ z%&;~rZ$p@=m|vUC5(4$KkHBSr*bC!lf5D`M1~M0B)^8t?{p;Cr*%^PhNqk782w%xQ zY43z@9!6{*liPR;9AY8C$r0h|<k4KwaXFATHJDFg9QHW#<|SE23#3p5bh48gtW9kB z<-t&ew|RE>|Mb@dij&1dAI_AbUc~`P&;k!Q2Z!hgQgKzU%Bj=LjTPwC#0hI$3(xQN zkee11OK>#5S1X!^<)eRe7%m_ENIQ|?B}Rnd|LYGH73Gl;T)HhmC<|EP>9Cv)mn<IE zo*lcHqf?#B(Q#q-$+J6G&M40hwm(rrO8rJ^P;59T+&|}w)HJ1dsSXJIHyjSb&jRk~ z18n;F6_kP$Z&*8U&nEHtL%OPI7OO8FvjF8^sx>KxyZIp_D8YZ2TD+XRZfo`s@i&&! zBFpqN5A#?j-Y-8kuU8V<U8dlSgujxO-94D|jSf2zfsUkw3Bb59AYOi4lU`cC_0^vs zPF240*Gz0*yhkODnFDEvW)o4~t13Y~p3gK(q4P7Oj@a`UWyic?Gjqw3(2Y@7is1K! z&=B|$&gJT|>JxtoeC(D5g>?l?&zkbvDSf>e;pa>wAKZt(&R=nvD2!*R+|%&qC-gA& zW(&KF>xVkIBo9CI&V7hVw@FwJjYC&XdW8RQCz6tS4W!ZeRkiz)8@KkjP$&B>@$4g? zVUX|}0$F6)-XCV-T!Gf;q(QireqLR_(Fg1!kU~cGj(2~YTu`IPr-O+n<>ut-HY0m% zGbi{&e+x){5D{`zl<CBoefgC+_kr4HZpWstX7ZkDivy+3`Rt}h%`TOOo@g{nNWmzo zHJ@dD1;oh~xn_|x<Cq=Zu)JU6+q{PIvprYKZVkR;McHU?FiyAd1AtXaY!%apZ}DCB z5)>~2kZyk*kE!_c<~8!6d*?af$VD2(z@;fcjugeEhx`1pKr!xj*CWSR?z}VJ`G*ZG z(E#NA;zb*a?OC_0wvF!;et(s`6yH_fPran49&w3L^8O}KajU-JlTM6y>>4?Q?e~*z zU1_`tSr~UnlAb3=5kjs}p8O7WKGtZfACrDvX?}mDbnSR(&47FQOgb=(F55AV)7<h4 zk?j1J8q05W7v7~e(Dea5P67kRQjc|2a*1BZ@)ll8g&&`aEGJcBqD+Y#WYsk2SSGI; zZza(OFj(1QMt#m8Q<BXjp2FD?>21P6z<A_gMtT3T!hc&}8P)~M)f^IfyTp^(DH0NE z;2wWO`?Mw7V^LPF_G0Pbg=wO6xm)0kUYPnVd&A*oF=DpGx1Cd{@<XXnBkc3HM|0QH z`u@b<^Si+bBrP`DVkw)=*NKx-=Pgn^M$N2ylGC&rsv-XJ=Z(%7rFS3xx|9HxK;C;P zt`)5xe0n|fD9lfHIZ`-3=ERQmqz~p+`#pb=`6PClYDeIzvBrCAs8uGC)|?->p4064 zlm^j70&>I^qvCTgA?(e;lEf+>P1hp0lnitxG|Uk8BccR<bA;q6%<tulqYDVDnj%51 zW>RtX#geb8Y6QWd+yckp2Q!{D&N*HxtIH*11`hkT>BovW$ewp>UGKAWjhD=e0lI&3 zUJmKQoCR`vS=2}UFbm}<$ye5Pf_V(w4TCq`PPO#{x#Fd){PXX}K_xW7x>=<G#C3gx zs~!VnDD6TY;vi6#gkAHK!0=t@d%lf{gYSxaIohRdN9zK4B*n*rc-x+T*$m0@4q8{_ z*%sf`zCkghDu;(%+Ia&^gr643myUmg_k+RufHgA4-Yy5zauGbNt0qs5F8l$&l`vMf z3@jo7`bxkL)w#8or4<N3osIs#S^$)7fj%PmJk<J65*jt$nW}H~RT3J!5~70JJ#l+4 zX$6u><AFoG`#kDB-<tmFM0`?qG-sF!eXLnLgrt;<(~7PN#bLymdgz-kbaa0OSIE?e zpz>ble4l#bH9&U7!lhXOP@&$uJSxxKh`83uttCjs5`-Ot%agEJog}B~>a#<ZKtwzq zBA(6+)0UbmF$`wa<gsD7vyK|LrNf4fD9<h4l@M(a>I#Obl=O;PU9NZLC4h)?ti?oM zvaGTSIr*^dg}JEUlP91FHtT=D4ue~0>A+Xd5Pj3q0p1U;#0xvDxPRn^b?)rfE!A%e zKDHKi-W|V+t!J@r!@Qvl`gs&_Y>%SD*;%o5;|~wRU6xB{sy^Y`@3nogy#K{dz4_6n zBbpq0`$xSPG%ue{Vn3rH^4s-47ZB=Mv&5UFiV5mUCYtSVx(K{pTs(g$C$gEfq0Qgy z>r^>L4t73&?3(A8yk4^&vd_Nag&{fjgMc2qr>k7!aeioLbjnH)zEcdE?3Q=I2O1>% z(DXyhvy_ZYkB`pM&GFm)H!oI(#FvO=Z#ylxGDDv;Du%YmEWBEu;AF_21yu2h8C|UD z0=;=on0$Z>JssLZ)$@N@lTMF=*)?+saY-2QQ80!nc$NAJW`B-Cv>gPA)5b>MtRSk- z^&iwto_v*!$3AO;I1~6AStvw_0(Y!8hj-5WPv07BjYY6d-*#1h4#|$GF6;jo&kYsY zundV5oGTbk2B+=?39?yjGQ_?P|B<fzskRGRoFZ(26V*lT?-75i<%%cUJ1XJ+P6P3a zZlL!v_3DWj%r#*ifBysSh;*&4O5T9V!t4Oe9MTE`Q?V0um_M4V4C>i|tg~SJj?oEj z1oL~*pbdS+6bpdMZ!X`;bJHo;x`Had?V6)4GhQ7>T<V7Ij8@{lc6BkNfa>eqItO-1 zYh8+Ob<zX7cLjeJSDXnFzt`t70*kNRH8%&Pf{KR&Z$GTt2N`4te|}N&ys~h3(ry`V zV1<olOPHpt$l7J2rmu01V}t^Qy8n}XLNx-8K#Lj7E4@@-<R!s8Vq>THZA!{CNo>cj zMJRBf^S4Ba<B4!Y?tYw}Su*kV^(}P-a?fV?IVKvhfwX`68P_?ztq<cCYOca`TNX<e zAC5<)E$^&X!kmhr)vb8&YQVqg1Q>ewn`-eVdBInqU9wl}$%<YCCQ5a0y*AwAPOPNg zEPoL3HKZNVOMhjmH}Z7d(M2S|`_;n?-DhfO=?jxDPmZ2b!!2t<o;Dk{waiw}SfP4X zxDyn!!#95x*_=wDYY94%16SC2EEYSYQp~cTk(6|n{KDVyt-mi(z56k^($iqNO+ua= z#dWIIPhp2Dx1^}WOKirbebabNZy)fS!Tn!Z#=6a=`%7RGM=d2CvoNQ4MCfO`cn!)} z%g2!r(p6+PXWT#-0`gYj2QUu*u>$BBAt#uN?FN4*nIyMPFAkYPtwO{VkM*WpWIV2g zRLdK&jJ<&fE%YUXasN*0Nj0`5qw$D$e77;wsTYUKXJi_??rti)-nSYv3NbBEkCsV< zJJ|gK5Ep`&HHw;6?FX^UlqVcDSfcBq!@j0$6iI&5=nAhI_YAOfw`81JnRNG|xm+8| z`N)4rU_d^spCBnx*}-I#HTOu#_ND9&y*apwP(#E)KYlCcTa9jOVNLUwm|N}DTw@3G zHCvyLM+qzPW>lA9e{{3x^QXF)RFJ#ajE|^MrQMrXw<`fM6KtwA#odEHJtm35KHy{% zUh0Z}H~FR@^5Tq`B_jT8IL12|{B(MEVD^7ag-cQc3!_>4hj$*9`9sOpFK&o^j9YS9 z=FJrRg8CE}S3*8kPzk$7vNI6NbU0KlvN@&`zCchO$LSYVIvxeCf7IL>9V2W8I{az< zs!d+^^jw2oMXK&qid|8{t(t8ECOE`ic?l{xkmi}Ge#6sGf@7j>2h5*<_p<yKyi9*@ z$qzSJKC$DB6VzBJVjvkz=NYr9Qf@@|$U>MWRk2Je7Z_<g^UAH0+_y%-KyyWzWRJlj z2?@_jhb^hV*|-MZzJo8KpXkW$7J!G-@Up{S=L=Q(_}_oHbKq^;pC8K7h`Ab9Hjq(r zSQr0NiYZj57taD81>O*cvnT?Ky={Ne&6>VxuQCJlp#~MkoY`6z<}(zjILK9O2)tL2 z$prv3nqQ7}LGot<ec>hww<O^eCXEhJ;|}QKO<T>MCx0CK*IOV@-giy(a=wzxkcH7L zP3w8S?~-H)ez7Te?|;8)+p=7=sgS=eYvFR#z$Uua(Y<w--nq~^Ne#a^pLu_qe4j1I zdq;bRcLxBqzXiT&;WrbLy`~v3F1^R^nQ`V+-;Bv;M>4T%<W(_PO#f!NYrl*=bd37@ z7<fFbHO(7sR<%Buj^iA{Yc@+N1x%P*C))j6!Oem`xQmjXnjsKs8fBleQP=)aECT)z z&AxB9m)uo82@!3ea9+N;{DXh`ej4_Q5$boN!Z(h=aFctjiK8E%avg;v;I0;#0rf=^ zz^wvNkRsXa6D3EvobL`HNgQe~#e`T7Ji#kOi{I|B!iKx$+N!X^g77u6<w25N!oHS( z5%P?M+0r^%>rAU}&n6HmH5uRYs4yK-5Q|nGrB=0|qTC))SRt7yr7VB1B9#F@E}}+j z`I2I=;5>mo-iZenLB|-gYipxH>8K&e46g>I$nTS7aWhMi9@GC#^f`K*&MfmZV<NSg ztBY$z3TMd0JiP!a3HQj+PTZip4D&3yP3%dfXXT_jOGu*BWqoE0iA<yxiGfN5e|=t& zyD`j^LShcdD!mP9p8$XLqy}&wN9{yP#?+HWgtBvFuauXL1VfYX_q*og!<8GdTC?;F zSq;91`g5dOQ2SuhFyMzW;f^^TJ;%n4^SznyrBx#u!nI;dh{5=IcO%Hus(CA>qiNMr zBRXRFxZ3*2)3fd45g+r+VbchLZG2RXC&y5hJMU=8=dvgHX#9Ud9Z${l0Cfkc{U_cu zn9`(;53vdc<G<6wa6VZ_aFQ5Ua(n^UiR9y$#&f?VSXW3(v!F64{4!P+o-kGTj_hgR zqi)}{4H275(s=O(-tVMz63GyNZQOe78+H>WVr(Q2&z#JFJ44Ur8Wm-$#uN{HZM|yy z8#Ee%F1o^=Wh;MRae;+I%}n*}(OLR3l&zL``>HKxP`30h3&NhIViaKj9=GJD?<x?F zBgSiBJ-ylP%71hJ40tBMZUu~1wjRU#m<HbWAW?qogUR8@;s6*)G9I&MYf@+bSGL!& z#h1o)mLU<1Oc8?C{moLDVsA?;ot>kI-NzM3?V(%0MC5-87})kjS#YCGA``}W3K!I} z0k3KYZDO~=<byyQcSQ{Ep3Cy4?7wuqMjGDyv{kJDOq?NwP;v}Jrr!q{W~nq?Jx4T( z&giEiCQ3)9yD!U~Aq$8ZClpDA_8QfytKW&m67f`}1&QoEhNSd%qfd^rhXn9Nz4ZS8 zaX^m0#~!yN+XA&Ux84u~x(WpVVnCh0x0o9Pq&2reQ3JjT1ps<Lg}=A|TLZl{f77r5 z(_CqxcV3!{uHBjlaV*3gpL}$6SRq_bMsw(z<&T8slpFKMn?rN>)%l}8&3(WbEX|AZ zM~2Ut)P|Ek&h*Ivj32{H^V8lZg>RUJ!Z_q#%<_+Q!`&z8+x%nf!pQ9Da<wyMD@lTd zu#n4BNTW1Bo__i{X`pP?ro23ie@kriIdZj6np!w)I;~oDVK|0zLW^#WxUF5gzEAkI zd-tB8eUeXzhmc_Q^eI*{eVXmsu_I{0q|xY>@ND%{Pgz|<gKRqKmP%<B#dA;Laq!GM zdD1!kH1tX4Ec!!*Y=3b?r%@VF6Q)ennnJegq!ob{51R`XdT0RPd?C)5f6Ul?Lh~Vp z>nG^A78@w$CSYusd$h4)q0hVbyJGVsFcy#_bTsmaG&Npdw~3WwFaXY6%#q46eR)Nz zDhP9R3>dp%Xsiv?AsAjPn?n!W|FCT8&9{jX{N!_ccS}1=LcwLrmiB=!yx@YM$#q!A zIU9F1T8nJEZrZe2{j%E5e>?X)37eK{ZI;~}6%u@r;pLZKW}7x|3W6nsSI`+7K@U9e zP@vWF<!1-_a#;YE1F*neyKY^u(}$3Y+c{URJWqnM^Ngd)tZg26>}i`XW9DP3Mul57 z@4WNJ>hI@U-R%0lI`DuHkG@Vl*00|ndQJ}7T?cjJCYoMXUvovje>6E6L@!5=w@Dzf zMfhudskZSo7HxiJ?V7s=^*&H&9DLK}NwptqW1Qo3(tfFaj%`1VxW6dP%k!2kR$tF< z-zI5pYL(_^w{9k8-3km%h|W$4z@$A@$Q@Nb$JQTdaQW7RY&aQfDkW6sBh%4iDo67d zf*v-x@f?%XgEeY0e<uqZ(oUZ-&8Fy<TQ0n~{PK5&uorZE9|1APhUtIn)E6GS>j^vm z!t-tBtXj>r-9d;CZcZ=8v%LEP;g_=)ORZn&4`^JGma+=%#k@TV@kqY)tABtePc>*5 zbAvD}GoC`fa^cgfzs_#0i&O{l51KCEf;Nl(Q8(8t4;y~Pf3J}CH*=GZ&4J<)+>{75 zQZNKVF-+OWX}_R65vWj3xA4G+j`(4mP&yni1%`1xfe9atp~R()nRmd2a?zd%f(G`# z<SCk-5H2|I87i*u6!i$WY9C9EfgklSka`fe3!$Rw;kY6M>DwD;aE)Q*YNs&gXJF7Q z)I;IHHfJ8Xf5tSAaUBfJV_a3qQ0HQ}o<5qxJI){QV=m2U4_qPMiR<BXEru)e6Ixq? z;br;+d9$8&fhc_~d<*~}I5GgV(uqqr{XDDyL|f!NfnU|8373W{_5EZ=&%U6y{{s!< zivViFGem*eAeqBFUJPTxAiOVKcuKkkHdi1fj1uw`e~eB*bkZ<z;`rLeS``?M#Vhf{ zvzSV2%g|71e1I3iH6JX-6-jEZ1bCk3Kyd|UFw2K4b%kqSD)J4?L}ldy?ZkL}Upy4% zbOm0h9s{L$LRZmS<@wjkkSCcd3@m)2B8WJSD^!nRrwi9$gD2Cj;Bz`?p5&YG4)vf- zhK1(Je~W<#=X8yE*=asl{va>h>7u#g8p_bmKxuxaPoCaB>AVy7yEmgx!UtgyqYoI{ z8~4a2O8*<sh%+3HP5&d0JZ`t%db7&RZnDXH`Ps{ZoupS^c}-up6OEZ|I=aNh`e%Oj zL&3=Ts7MQh^t*QL(oxFFKGf#a*K0GpQAVw|e}??}0GS8%*zknW;^&@w-rn<`8?-1$ zjyQJdX1V+Ce@L2V^|n;Pg^k;GGE9bXka+@krcW|n{KuImvFpW5#`<HFG`tXEaYTuR zW3Hu7vDi~U_#e;rJ`NHGSH>nOGxi3U(gATE@Nv0VpA7Q>tfFII<D`MnrA;%7OO9wF ze^d(}LbO4Hk2rSOs3Vr+B%m9N@C#fQ%05|>gvHptd(V4s5LcJlBac2B?A%cX$BVIz z7wq;(QzO=Rnl!OS$2n`n=W~^lZne`f!{lo+<3Mhoq^x2N8+@^otU730ApqOHeP^gI z!fxnFoxusm`?=>X51LwaI+9s4DY4l$e@TABoH-%R4cAYzjhi+GTVStNuTlCDK~M<J zO-;M)#v9*lV>MPCY-tH}U_*Y(E$<B)Yu?X8Mn!&(OM~X+H5&r2AT&d80iHPL51x}I zJ+pQV>ow>h8bPaJSx4pBFC=qe{<W+3S8xb;Wcut$`=y3{j5t?|XO2iH{L<^|e{_^_ zoXyjvCpd07UffV;%?A$ZsPvK0_sNrf+PtsDY9>l^P{*TV*CzgPJn##xrAPyRay+d^ zHo{y06!rz;u6TbF*MmIu30t<*%EJ-M(voTRUw-KqG-g!Wi!Z%sbLP|=0&RCy`d~Am zW~u~4(jbvm4hH_o1BWdygt-dnf8*!DtX~;d7)!}FI~Izl8YJWWjl(E+pgh8O0PP&t zQpRMJg$5BbHe)DQBP^=Wk(VyT2cMDp3vlre3Kj?mx=%<Tk@?U;U7cq+DMS5Z{t5dt z<<F>Bs=N^F1)9qT7CxE?74UHZ9qEC^xW+KjI9*5dCfYQfXW0J<M`aPpe<WQYd`1+2 zQ4jdYaTV%e{BnFK6Iytg1F46|B*n56u2DUpPmF86dT=}rPD9Y*bd7NZPjGS=@23OT z5hAV&ccE=QTu+teX&%T|4{wh`ypxya3iLHdn&#w3e{$X_#vgskMx;@r&?o6XArD3r z@t+&C=a_65b0}MFLSGIre?2F>LqCAuX|Y`Qjud{mW{xunl^?0vLp;LV=eZCJG7*ow zaGn>9hYJm%d}9~q>w~Dn9mYFllG4N6cUV4LPX;58biD_HE9na1O8OXAaBw>C4AY_B z>-f75tPouXLi02}ahbq^YNN`C<9WVui^TYxE?g7!&@5eso91!df1&Ge)q}j;<>LV_ zHy_ODnvXxo)6)(Yu3jE_^L(aHX2*}1-aMZ!n#X-DE;Htjpq&`m6M8fHB)9~pMazOY zv;6I}_-JTs*sw7e+01vGY2}I)wzs+2F1`ruu8A4DMjV0EEmjr%r`n_1#GI!ij#*a{ zv|$iz?bPwN_uO<te-;!yjTtj0IliHdxeLG0B%v+NJx|&%5{NM_p$C_4x6{QRXZqyO z`lOF%?$c*@_P)cjI}hib=XBdTJcc3cBc8)N-$L_bJm0(RILh;MXnSzHu1gvwJ<?8V zmvE{@#|9@#`)V)_?GJ>t_uczoI40O6!S%AU&$9VCzPL`>e~?>c!wX!%yReguvebsY zaNL1GW=x-9?|a{S?BD+F--F$~vzMK1Pe1dFA>^7deWn)PoDx8r=6KNB+AR$=7k-T$ zJIb1x_nBW{Fn`|saJy!sgk3vizl)m$(VBVhdvBBwBQaXvA%neWxZQB$b$ww2!L+ld zOPYCG)c-N?e?2yAlHzpG?M$B>TAw@}YuC6>(oWtsgYB0iF$6C`5H_@$Bz#6dG(#U{ zaLm6mq(y~5x@ph8u!*jaK)toK!!~T$Vas)dn7PQ^XnlC$HpsA{XG1BTDYky)6a?b{ z8gij;O29!H{&2wfQ-C(DNmz-5^0TC|>=$?-_`-3_e;C1B?AY{|38!(@$qlI?uS^6s zztGI-6{jOCJr;V}KA6T5T#PHUK&Xen!NZB;X$Lb`=A_HuNuC^Kge2sd`HUeADTDxQ z;<GUA;e!i;vm~=je05lpfAqGB5>g5VxrvmDbk`=JARwS3-JK%@MuX&3U{jIq2I=mO z(E}zmf11JQ9E=$8`u)AX_qyJHpMRdu=f3ZApL4Ero`>Ux)UTlAMA^wVWuPyxdfK)$ z^i`i}YC#_U<Xyv0#-{UN+0=_*fj7H_WCyOUpbr4H*@bUwC_5m-N4mp4GEH5u-`i+c zKTv3gX-NH-spF;1pj7bQRWd<?lqb9}uN$%Pe~V&I2a&%WEReHP2t#s%JpupfxxCH` zR^0|bXuW1r!0CgK?lbFC7B+2+;O>Y%U;42`i}nppkjQB3ueu-igPa@i<d^Qaz0ecO zkv<yRPxEB8?QQ1zUgsI1-ieNY`ElEP%Ns6%r!GUWa%ZWnSUT9l5Gz<1hZW1oCzoTt ze|@Z<d6T)JH_k!X^pNUH6(APAwP&nu7!IpX3-8`L5I(?q@{@r39O`USho)VRe$-uv zfVxffSjgaoaS1(a!vol^79@W->b==nKch-~o~xk+=tSTIMmxE*jOaK^zFT5!av=e< zT@}bP*p{XO2f(piC;&HKXXmU&=dx#Df6p4w<aOS}yWa8;x;Swb)KEob_mFF)4T{em znfDH<R^yN*EmsrzM-lN{U&JJX88<leO*k(=y=>17rLjlL>=^1eMH0-%GLt`6UT!3H zDC%siu+Y<Lz`TS6_&#One8Hpk$l38_gGaU0#<Jj;ol-{I^2w>UKv~_bY}Lk~e{k;x z$BUO~zVXCPm_rR!|H%bwoJ0}BO8|ITM27bEq=b4hh_CWJ%d%zw)t+h|Vc+H?`n0}P zPnS<t=8-pc1SI`}<W!<+9D|6P5}a$|=>8=qdHd~N%dS4LPZm55fk(BIUsmJ{$OC4- zeD`M$eZ$AW^a+>8kPy)KbG0(|e<h8iQI)-x3B1HevDL83)@(gO^#Nj^5uH0_&3I2~ zAYHqr*M)oyH-VxE^iQw^)b$<Cr-4j$jJ@^JZt$|^jSzVp#KJ$1j3{_5?L<S6j{TYx zc*6>B%d`LR#hY9J!1UmN6QTX+rV1;|&ShNy{}%E+a_ti_J1(VX9FNkWe}#3Qp1M)- z9BsSKp?XwpZ}G)c7-{7~!6mB3HCw+PJH1~WqldRufB;$8BR6YQ+0YDgCs(;jhk%pE zY@S6&*MMt=fw1B$)hE*j(gskWaJHk*WPsmcKr~FGi-(^dEu&ZBYG!z^-&^W}npu3E z{D74BY7=KE0+Re3&gJ-$e<c!qn7rum8F1jf;j=#s^s>R@r&6!mdLSA=^cR~vYG&6R z!%xQAKWJsT_8z_rA=#6d5LR%jKl+L%x8jUq4#u9GGMb*t4wG%0U-gDj<BDBC8;AcM zCX9{k36gf3y2?~V2IHRUhka(LJSvkgZdo(8$~HUjy?EEMaWY$Re;55ie12qMY!&%W z?3~#PZmP1fb$@EZ&6`lMZ-1=%GrS@&O445PerRbYdt?aJRh>UhVjVgUp1-1<e|1_1 z%CsVt2}GE&nLn6ZumH1hkm%6qKT8XpuMa82%J)kcB?nUBC3e%Ow4VWKQE@0!Gr)pB zN_dAv19sM!7j-G}e==Y#LGE#)Zyj35Y0dNLPy%@~R=PQv)DGw+HMk%P+0HXNkVdJy zjb)puUyBT3FVGufd#N@U4dlRydVFKX^5$yKnOC#Spg$t%f1G0u-4np{s1=EFZP{4! z+dJ#uznDd!z2k|lK7s20A}$qw07IiLMFP9LyH33l%J*E+e*sc+42&qn6rW#I@@eYg z#!2BAjGi3c;XscPQURA(b#qKK868y;(HVJiF!Z^ljyz}TMxOYoZPfU1GFzh@ziPzx zQ{ocs-rYXIiHH<|NCpeAqYV~ris+?w=GU*wFO9!zN6r$rPq`-Y4xG0jYy7{bmmXWg z(k`ZY9<l^<e+Ivx{+~xE?*AVfL>kr)jXImh$1I)I1M~0#C)lH_<bTE_6Ze)<dgerE zi+17DadDV@!6nw}?l=6==Q&yd$fICVMVzkk%z?$kXl}$Knwn=C(=$INNRT9rmT_qO zp9lQGj{_a=POrcLvifdGVw;DOx9Uv;LLOTf0?M7{e|-KM77#n;2gx?&fHO4^*ieeJ zk}enJrU(s-|HVmQtMFof&O@xC&h&PIrq#ffu9W?}=Q*Cm=*OslxWP%&{j4@ViiZ1s zX181QnM_8KTyN`3H)pN;Bie?K`aqwLM*Hn>Ai)dC9UD!XG#NOoYft8JrldccIG<#) zAL^9>fBUw)0R^+6(HP5X5~_?x@{`rfwbK=EsDrf_;!LX6dvvQZNBneKy&I4Jo~*EF zrb<@#K(}-)gDE8}#l%h7FNjgxuYZ!@fLcw?S&U*ItcHJwy9**`dvo|$OyI2H#4T&J zO2s+Dgj@|4#2Q0wbTiV`yGO3@Lq&D3YpuFne|e@d9g^8LHMQqbWWLY$DMz-gsDTTd zERiQtoI(64pVwmEfW`>lJRp_tIU6yFC2;UCsg=GnjWsY3l{*abW}*GIfVElt7h%ca z-p0GWXQ@34K}0uDe4gh5_PjA~DaB?ljtLZk^<tx0`UXph#BB)4lO_OR-t|{BQ)&42 ze`Ogjf&>8YPr@5@VWww<-`)GqN!^lX`9I}iaUv(4Ym*X2i8$AJ8R3PWa1n_K`jv>R zoiChbdYJohfp-6uosxnf?H>1$TIMNJr+!AJ`#7FE2mUwjcj~Ul^~#sU$7f>^6kkM) zxm@qIlb`&Hs4w0a)9829+u=iKfqmwee<j&$CSve|<=s!NIe-!LR$5Ptr>U{CGmkUv z^|C^fpTL+T(6{knmJujEE`|0HnuW#O&wT2KZHY1s@59nSThq(;G=YMkCK!K1u%<SZ zo6PK51<BWsUvUcUqI3VeaR62bdz{51FqXOz-B*Y8d-ydmW6NNio2(yLWzquLfADYt zJuwyx7$W28kxu28QJx=xE3+i=()UDB5Y)|C!m(!~OCd{M?BdS=l0Ki3B47Uq)$Wb1 z&~hIj&KH_5Q^fGY=sNm(9@mbV-b+rxezyxW(^#)xGcI!|t(PLRsu~msXmrKm%0`#I zzBRhsPTvXp=_q1V<2Y?kcFcL5fA3OQR8)l@jzDN6Ix0myF5*2B?+KF^fu;T{T@yfh zU%|`uFm}XI;FZfMgWW>8w}rs+U@l9>eL|jqdDNHifG92wB@d-O2~bnyh)r&2<8edv zed3#0O9`vbna|&}@bZ#D&W|?4{4wsv3-02%_C;#(5`DX;SBPx|pkM1ye{tbJ1c?$H z1@g7auT+ZvBs+h~{MDiy9zOGmMZT0(iV2gCbsYK4EPP<0WCCA<05xG!*3shLhvN6H zo>g7F=hIK(Xnc2g=m-10C2f}K&h#L&xYMG6{&A9+lDm7XKjjw4$#mxjq~)>Y?N;sJ z$#cwxobUG|O%HY+#eBr(e^c0IxzyeIlP{2;VpF@g%jFF{Z6i^xsooW??&gYzJ%Tp< z_~RSiby?e8@y+Tc`3vlA^G`^QkGA?4d6lT@*?;ihYMf^5ecj8EVUJ4&nTriig@E<3 zj2^G)VUmIA7gVio)08ziGC=;9e?yL|&RJ`EgGZs`Y84mJhf;QAe?mIJg(s4JEFEI& z{eo7Gz8|<@9u6CeA}0c4?*V)RB-7>MH_YSv4}?<G{WpXJ#=Ne+HH#gqM#%wiAOR#2 z-%rLq*xASIw}IY%juu$7^ZUAYmS@&=($p7i10VB=CoZahTJmn~G2o8(1x^~@1j2$y zdbVA!1T$!Xf~0oOe>%~GcA-<MDgc~^>$wd73GAI)RB=ll2S1AclOkBKB=ioN<FLS& z3h%xPWBxR)spxHOR$s-V=%%4CEjUDhFl!_3FLfr`{zjCPLz-U;<hgGSbe#-*>7`UU zf29+pdQ@NKt8};3?}znN(2aYd^NxqR@U^<ib80>Nq-=Jce@yWQNio?O^bATjE?z#b zbr)#+376nru`&Br{>F4s!3OGgt0ckwCTU6Hlvj_H>rgS7gooX)C)&W0{smV~Bfb*9 zOJ#;9i!bD;HiAbrj6NiF$M}qlM*FyMtH~K^*@y>h)b)7zN}2YlaecG5LmZMGFzB=d zc*(X!Q#|qle<$)hUy@i)QE&?r^jazdE(#o42fO-_s<>_5VwGiGC17bY=qB0YaCTR3 z*O^Ekje}M3SI6WepL4JJ7Sonenl65>#;_P_bO8U-e*6U%L7cH0&{DJ9j2#hO!oLh0 z$@CY|0ZG<bpR(H_Uhh9n1ijH&)B|axqK^8|Gu$Nne`&wLq*L6&*eBj&waW+WFmQN* zMn{sw@QKCx<Gc-BO^<5|z8hp`Ax3FrKtGPz7ZK%qA-ph=3FsYP!)PwGyGd@x=buX0 zi;i^*oA)mwmIz`_6+B$0PX)z|WRL$=jwMQ_&oClYrmFkuM1S#PS?Z;f5;@7~AHCE! z<vGrof8B}X9U}@tzKq=C`Lw4p{Yrsy_5Icd>Gaa;Be!$uB#Iu#*#z%j2N&RB8E>g` z4r;KY|HsSWJ1mPBzT)Wah{prNBVH7)4IO@jBQy#V$%rO8351~nE~0&l%;s4fViuve zNJhD-Zr=Q%3WUddCJX!;id{cVAwA;NcJb$(e=$R84mKkNg^+DOHA-94$w&+2-=wH( zFUYwo`DV{1Ic6kTEYSPW>P8bmwe9OjCiRd;Oq4eF7Kzu%pv)N|iQCTPG=E+HPn+R4 zS<?xNIl5gAK5r#oyPaGJbayN)18Dt)u1kB%*-)3Zc0+M`B8zi*x)Zv-uWuj|s6KKF zf2%zms{kVwLx_9MRlAx!MSXg$Kv3BNVP_gb`DnQEGLij0m)e+XpdorU_sPn#*wnO8 zKjH^P<L_CgCciTwW|I`D0LG6SO2?Yxo#Bt>ae8uQMjJIhzA+IPZ^<i7B=cP?HY-#u z=5~Hpp}=~G{EjLut-gPx)i*qSd-@^ve`CBL-Cu-u;8WJE?@PL^Tw5EK*YYi9s8{1v z>gK}m1O5($gprSY<X!t(I+!@yeKI10X_JCqDVZ-b?563PE)1|?pI^_j9AJJCw=Yng zm+i=Mntejm#sbVJT6-P8=HL!v`}*~S%CPOR%bgFY!ngLa0gSpIFFWe)o*bo)f7w06 z-re1PP6Aw)bUQPlDfuKA#p8YFN{b2hT>mXMZOv|$QMxy&qU4C6DxPD$Y{bytdr{<Z z$$cYVt%Fe9zvO_^yZXBIGm>?=@O^3Yfl^0l$P%IsV0ZQYCu3qn9?72N$!|JIj^p&3 zEAb1q)Gny*hfDTPff>BNn!|gjf4XxQBuiG>8yiao%e4cMf&UER$L%q4cQ2Duwr813 zj3T%HR@SIgpkKkbKp}Z1F$3u{Wb`uOcPO%!fALYhvail=j%VO_KK|<FXwZR@E}g<@ z{-|H)Xo1?(9@}rsE<<g$gX)id1zWkQiFq{PSKALVZWhqj3Ej)y`WfL+f6==KbI1h* zefw<^$x|oD)>ZcuvT9x6C`)HZ+|1w?7kF7m$ns9-(K-L+%x6TOC~YLl_C9d*YtypN zZ8m+K&#Zh)>eBsd2$3M@AtSYV7|uNUi^Ds&eb*~fcD-cWU*RF(xw$7@BANc5^e3~w zftw3R;QlMVp~?`m*Z6mre`Nh@X2EoqECzXJuUtG5PYzzuq^&Lc<Z5u~XY`#OJR<bn z#3TEN9R4W>3Uj=Sl|q+Jr$ml{2V&IC6Y~>JWG8#ovW+z}e<HLuBHVV>w7*MO!o(;$ za{8jV$w+1&Rv2}-h9(GGW0)Nl$e{tozL%n^jt`D8yRu`wwvJWee-lNOLKFjhtrs7S zwvrikc*DDH&H2EfQ*)jR%wm6Emc{ZKm{UF)tZkU~@K;9P;ydI6rmdK~Th!qUS-Ef} zVMxMtSP=f%g5+_^6n8W=6v`(P1L`wGh7Q7>_xSl>*;qfk+}R*ce0!uOk}$H}A-PZk zuoHEXlg*|ULz1KPe_CcYw>A}6<~K}s%bbyrJaWXs-P%gmyg9eMPB{x4JFvN0g9p&L zGy~nG=~$0hOMdv?VSbXu$)@>eEFwH|^P5b>p+sd=gCKNr*FQ*+#m&;#TDTt<RP(MB z><6KnsMfbR)&DRQTM0KG+-((;tb~TCOFB%V1vUNz>1mEBfA~d7#Rn)au(LWWY`SWh zMfsh6b@GzIIQZ5DdC<fab-|nAdMTjrZRz(;7CE9``-^4<m1Ys4rb_al{rgJ$y7-C~ z{;F2FMtt45r?+<jpIzO=?&$1zm474e1d<6fUo=xTFU9HN6L3sfqgzW;8V#Kq;BRFT ziI=32)s`{jf9J)?!x3U@FB5HiIarjKqYME4{NelcAMp(C(wJNW<8j*l{wZ!wU_qyv z_YwEFS&_)x$k!0$G>A+iDq^Ip=$%rD)@b;&@Wa~I_^jKWQ@x+kp7;fWUYFC*h~Ico zDlq??B&Z{RQny$9=y|Ah!|hP7r$X0@z|*hQ;><_=f5t{Ho)pA(2H$v-u*A*s;~z)k z`OF98CpNcNwmS_+qT8Z>%k8rT>|YuPiE(>Nyi-;%ZhJ<5D~N}Wk%Z3d^{s)w3R|o_ zT+rK*qHrE>0n&%vS{qH)zn4*RQyiAP5iZDa{A*>lKv}vV+p*Zb3+(YdsSKmHun5<% zE89t_e-h(BbDxB^wo)be4;|%acl_~7n^sarc<lG|U*!)SKf6+2u=y?VT&;G-Ky#BB z9$`0Se*P7+TWbT7WP0a{{swR9jd9j-)qkM5rML;iD_#4%!iScGZ7zwR4qw>O%?Fjf zB?DRu+(n()?w(2v*;$WCl$`dy>gAu~{<iKfe^ih`r?|`d{30{p-FeWIe#`H5(h7n2 zLNm7DLe^V*21mE(N5TYFf*zFfvkI&#BRv^HI0wJGncYNnxoK&uM4Abt^lZI;Pha-@ zOQES?$;!aW?+}f9T%4zEH}Y9KUuFP&fcHum&5RffwG<<Hj?<xk>0RlM<jX=SkAEya zf0EOF`n`Wd$wdIwu62-6k5xGTG3*+(d2{Ig3&Hlsf0W{H-?Fel#Nn0`-<g-#n~+Zi zT-+DXz$;~b`>Qlbu46lDka_Man<>S0{Olhb*^;cxN;Av(Wc|E$iJG*AG(+7oh9s1c z;PvQE>BU65fu*+bvydhil3d9u%?n|Ff1%~on};y8-&_Af%ZvL5XRppJD<tS?zvnHA z&bk~ca|s>>BNA>G9))?IuY40L4QP-(IMsyaIhqe8i=z4Jy<#%CtjKPMp9;G!zn)7M zQzxx8sQ$56=IzFO{<&jBhVUH_a9W;T-x0SOOG4(D36FQNU@i+HZ;|O-wc)2#e`>;5 zzD~|EWgn;rzJU<_E5*jKF#dN|oHQTl5HJ7^xFId9!IgB~<aV9_DmZ&h=@~U?h962V zZh^O*gx=O(9ZOy8B)!FV%CP3=yCbT@MX4EeG`r703C~mS19);!`@{M1Q<J`xtiiQ; zIo~ABE+kkTT0Y1=m5%+S1+Q_7f8`@*E?5cCN6h)|T`q|!IeD-oT4<87;p>fkEwHgq zZY(N-;gQP>5k#DO^V;A>fuJ;5k6#*NT=YRx8|Jm^aFLPQq{^Gg56=_R9CtP?D0hs# zijDj(?+&;mBM7o3h*uWK-rbxkxqhp)t$3;#vYQCdD_p~h!v2c#*qEc!e<&=upvE&W zVkhn%RY3m8=$VR#%YI1f-GjFszTcwMsJ0fou#G!Na0@c=L~LNfsIWh_8Js|_TYhLr zQIDd~Hzw1ku~q*)q9&j7nWAAp@d1eG+h{;lZ$GCcHiWvvmj!=Kz9gyck-~+(rF1;{ zHR+@Dr-=hBT@B@$db1mqe|rUAtK^fQM}*-&?$4Df;_i*UmY5hL6HyEzc2Qv6Z&$r> z7=K-dJ<gS>S$)Ar{$lD6)|P*HzdtvT8WemteP!SUi^RdX>8*yv2$>FjV-fYwmPV;X zs3kH&YM=2_lJV2;_qx!MRL{0v!VU&!Sr>1{7M--eG308n8$J<we~8->K4fLfFr=cq zmGz!}f8vTuJ+CU`N=4_vWIn*oumCKtf*cQfyhykC;YB<Li*iF9;E(cM9TR#kJNkCL zbi;gh-uMx|cF}2#KR=SQ{ETV81>6j)acjtRedC6j#B{szzH?jG$&~65p_Tbpq4@u$ zpuc+dr4Yp^NIRk(e|E}VqLlvqklIf-6gTo94uK2hK7RY#^W0W_?g-}Az*Wb!<nPY< zq-2E|k0ciq8Vh^v^^E1nIo)3<M7wP$aEv`0)>`ZjUZY-W7L*y}QLO8<(3Q`A8L9jP z>e?XiwXKLz{5=D;HXw(+9^2s-NxNqZlNZoP;!o)er`JJ#e+z26^DK0!NhTG2dZRO= z;6a`$dNxd(ts)?~q)8L;QG2QX#oy=j-z4lBe55YgmznaSwtn^;4=vOFVcg8$HGv<A zd=71laOEs|FFRJYw2Y{0s+As!I5-UCNc5cvL2qPwXMmIwhNUz3Eqe^}=3I`HQ4x1Y z^q+d5E17X8e=TcVGfh^Nvcy?DoqRQec&Vu94o&!yPKoYPHskN>^(+J#HLZxQzNHS2 zYot*_pNhVYZSky+gU87GCG9tl=q6i<m>a0qfZyU)pFe9|rqfYBIV2dVnYo2fb9tJv zWxGm4)lE7`r2?9?u#0^z$3E0mfGz~`<(d`BCcR7~e+{{@&<|4fKaKczDbetUoe!hf z#rWn)ZA%TG%*a+_-Zo-Y5Az2<SkfjLM(>}H(1IR_wJ~^W7((X{>wdT6&TMhIrLa~; z&||Gdr@(7o?+CO#5?)n%$3U4@X20zAeDwBa;lGC4(==%M*iP!oVM}~QZVoCbhXj{* zKd8O$f7(oK4g#aO-H4wPQ7Jtm6%1+g{JA`Pf|nM$i8zu3ZtMlt4}IEG#$e*?doHSE zHsdY=h$_0v=mO?q_96GZ3)xoeS2x-@dM?E@^-v_M%xrq@Ij@Mvn^3zI<tvHQ>U<iM zF&F5TkVu6KJ@>l`+S#jsIWBL*4sph1rS)8ie=H9kL7Kp$!R-tBTa*{+2~7cLsC1D9 zDvL8xVUe=<{+-AICQd&QG26lE3~aQm&L}@zwJ`!sL61blj9zavc-I)Y&9g~A$uO>8 zzo~W>j99#-)Lh7M`sb(GV61{t2QHCc-q_g|-nmxr`c~YOBx^o9^TpJ0b=<9g_fs<5 ze_yn7>mM8eg?lGV8AckJc)$1}Q9s2YSO6~bBR(*H5PTm#5J#h%uEC}LH9w6ZEn_ip zMDDnk)4R9jy=>FArA9)IMk0=L$uJu^ysSbFEmpTAd<t3<9&G{%8p%37tLw3|Xy~aU z_50q-@_n?LAyVug&jsctNo-G|j6Sw5e_zNY*8#E-Te?(f=4B!!lHMPC?wwn{bl&%Q z#Kg{eRddw5^lDz(zTs(D2Ug^4*(;vt+5*5`y)pyvn@Azaz$KG?V_psMt};UfZ8Ee- z(TzGl;wjAl^$;Y&-AE>i=&Da^F=lm-3jK@p?cu%vBKp6e`|gm;jY>6<k_lgpf54jU zA~$3QFBx#CFw;=%n8?2{N@R%Thr{YM{5%1;78v%wF_4-Ys#Z6l^Cn*AForDBWj643 zBH8OBY+Z|%2<*{?Bw$xM;Pk(9xpJDEH-M^pro(KUR7rYvzLK~5RK2nm{&M+p+k)4y ztjr|Q{3aR4Yw4|`?j>^birZY!e~PEl^V3dQ?p<_AoIJy-+p)>b+uP>T&75VIFd93H zk%E~@>)z7<8;L)^Uhb12E9ceEUMvZmU?u*&&!tTU;pcseoy9X(Ls33Pb3J;5o|+0A z0Q{Pi!C&SgB9A}lbDa4JH(--g_nRSe8ht8LJw}LJg`F8jT=#!plI~%#e=<38jpO?j z|F2yzNhYXMwCC`C)+OM}GGJfKBAkLR!qTo?TI)h6dn*StNw;bot2-+s!%6k)`A}ca zWC14lm{r#~1oj^>{{zk{#XoQoK#a$IUjgdkCnV)UU;r*aMu)CPzwV>{i0x@+P$)qr zQLD6c#vGl^DC=f_g?KS&f5*a~Y#`l<u>v}DPNZb;!(u!>y=@qJCM<Eb<?hB&tMc?7 zfVcSVmHK!_d5^BJ9JF8J*PFAT1<_je+cPZ=wN)UW;E8u%3SQM(g-tLbe@W_p!)jZO zN$ilbQ;T4y9_HR4C`&sg^9`Dj&{C}(%prR3FQ)i<VjQ-_X37r*f5mL3KNqq_kDKE~ z*vvg6AU+m|9mk#OzGGoX-{598UuH_&!{ycI=*|OKjT2U=*Cf;c?f&-#mwFi|HM&?R z%9hJqDkYDbC9Wq-Km$G6U~sB4)ucyHK|`avmCCinAlsMwX$?M_I_$3zviDDK0)bH7 zmOm1M*ajdB*5F<<e>FcWD5ho2aI!0Du9>H{$uomq^p!eS``p207x@2FOA0S^A~@Ms zX-5Xx7%H8R7Z#`4?=+t1cUReQLlpSYXzWSNUZsBu*70!aFaWY~K;Jr)=(*uldb?{} zOHWYOc~Wksx)yD3V+A%1^CN2m3IeWJ;-)cmo%2)vfBQY?e|N^bg^LWm<xLP+yQZd& zGQfeM8!(Dg9q%Y@Q;^Dg{I4dyyICQmpKRB<Z#7Q=M}ZgqO0=hNY<SWl$->!b4Ozrb zBwyisRP(U1&{Pc!x4P8e!v79t4&0KR&m|Wc*MOZe;1dnf2>e&9kCzniLsKcQEKNXI zi%7j{BG-m)e`vS&itmZL;2^p#P)IjfiDj}AzbdR7kMvx?zazGx&8~LsEWp$e3*K<M z)bQ>Ljnpf^8p<u1?{c@SDh(dDmm=Vqjg+KG1)9~P*f^Y<+zsa~*3Pp1m$N%BWYzaj zcDfe;F!C%n4d3j0B{+z8yS6NoKuDCpkZ1RL7`{V(f0*XUnK+(R1XI)0KK!wAKS1Lf zZskjszu+fn9=FO*bcvU%>mU3s@|&@BvWM&RHEuRpjz1mC@8Fp&RUPES<bLKQCMLQ> zWXASinhQFM^u712el!GWcoPvuXTs`jgCD8$G?{A4qQ=&oo?9Jq-8S{Li-ep0Nm$X* zuZBl_e{qO&G}u3$VWk9PCRp7GHBg}LMe^PpTAJ$mUD~_+<tkO8dZE0C#z&h#+2;R7 zL@?uo=`<-S>L?Y+*Gh=o<}#k+*0Zrr{&Rl0&wAD&KdPQHzpdh(5*beY$q?$9eD{o{ zP@d#q^|8tLQ1;qwmc#DVDUMrPeWKC$APoZse+(C7#|V0Lc3J)AL<C44r&u;0pN$=g zskrudTdTJV_WGo_PumN99~d<K>fW1yVtwYzsU2JUyB(Van)aVt{`mSfXe6FWF8C(% zySnOudQl|GRTlzdc>^4SDV<}WefRiQU(A$7=km#^<goZ<@}8^-V`>`FRf^UP{C~+A ze|BpDDeHSZ7w5}R$Re))>TiK|ulxD>D&5;npZmndrXx7nRLVI!qFcZoZL7rQrxJQ| zT8uKjfDfb|lX;vY3Euw*-F&7!_{Ee3%f#+$dC^V7%MTYZj|V2bD}SWKP#Eedk(yV2 z*IfbtP(ZK0zz7tL+(?eJdR7&qLUfR4n4z0_-+zY`U4}<D3*d&tSlO&3JR0Rn15VHc zZhjN9)`N^Nv7Ey0-K!3@2_N*r^q#S}&OxTE72W~50p6~r$G+GR*7V(>-J7zRtIVB2 z1NG>1c)X$KAMWv)C0w9FU(6>0!InD3qQsEyEXW$3xb0svDRzN5(}o!AoLp%{TwdjA z>wm&^`~N9@CGIZIG$Hx&&;h;eu$KavNz-@M$xq%3ym-;*{%b<M3u59>8<02Jfb*CZ z-b;JD&z98U*P*&OH_%Il7dVl1!jJ*NJa(JVSBv#A1ms@aTuraXZr#GwO2$J%wTXmN z>tzK0bS>bt?#@<zYHXbhHW}yR*tCX#9DfHd)XY3qeEQS9NrHAPZ-K&XtqgP>FT39y zMQ{)sb3#3n-`(M>1YdosmOV^4hR&A`*2HMtm*4PPK6xh{c!f?ZtZ}MFwa9e01U6zi z2A(=y4EiDg#A9hF#sX~s*+cQNbJU4~1NurZ5*>ik!IuVNDyt##$rEYl04{&{nt$tq z_|B}t?yM6lufK&1-8(vf1fQn&Uu->*Tn0~>j+YoZj-&a3v~DJ&x|P{Rf5+@bW={M! ze}99%H@T&6fC!_=#lFt7z0H~a{6#wZadkF(GB32CmH+Nn`eCg7I5O^VKj%p$gQe*^ zs$MREc?%^#wv|8>neFmX`)@A6`G43@@2hO~R^6R3JZU)3ZT7vZy3fY4<w|#6M)ADp zNiGIGG%OEmTb6)!hVgjZvbi!(RN(*HG$(i_IW+C~YZ$#uHYG4-sHZF|lW@9lH}UcE z&GirDq^#lJc{awH&`LT7-tX`t{L+E<fDE8N$16HiI(yMA6g=v9!>ecETYm|l>A+j* zfmFZ2U&;I#6&MXV4rq_b9_8Kr6-yo`$x&k&UZ^3pm)<SX8n6icQpb2O>lQohe<EvZ zlh*t<HSi#3Uu%{cLOlJ6LVh_0U*kPHxR(F$l=gn>;4<s^-B(dOmh0N|m&*a~^B(%0 z>13Od(q(%$Dz1lAzDz-?#($mAaVEL3P!u;x=cQJ-6am>~@pzkbK@wHmu1;k#Ib+3t zr_t)={@M3b=<xq6tgX2N$t*DPI)>%0E>A3KPL$8BWHintzFPOXLc+g^{{|^9;1Cz) zhW7)sR2wbROFqRN8h-A3L7Zm=lP7Jvq2zAFZeiTB_p2u-LT8@3v47=VLukb!{vuR4 zN62WMN?D#47tU=WnQOPn#eOz$yk;qA3BGwPd$*KHT0aqZR_#C<>{@%9nn>8$ifHgg zs!u<WGPS7cQ~ENRQO=QDuOx%!UPyKL2XGa;1)o#?<=AAiYbys;Be*C|%ESG%dvanr zY>jtZDd7qq@XQcQ^nbv!o8ll`2VE#kEZ*yMo6+!z`nAL}i!;%L8CI;9f`3oCtS?;; z&qRcT({E~}W6`uAgM@8PkK@hw+|7hYf1#`~o|~<X{;Y9jFBbF43pC@*e?Gn>V>%$a zD$?oO8~NEOrjN;C9osw-eYt8jm`1BKrGd-s|JSWca(N$xfq!ek^F(E0<&5W4+%-zP z*xs^~xBikJay8d#buDvgKv=5mvG?n43y8RkxNV|3Y-Ahg?|o>tcsbX&U1>7sxZq_` zak(5xGxJpSZZqs;rX*zbdu^M&B6m+iBf+WMMy$qQXjb1qiZbK$!%Y3o*BB4h8l6O+ z2cD_p@Og(c6Mx^1fYUyn?WP~<;_Fw(s)Vd4U&xy+kZ*SNT*=H|%Qd)^(~OVOO#bE2 zgrtA!P}-E8gE7W_XfA7~3D4{pxsLQ6iRES94wL1qv7BZpES9j;D7CAMV>-bUHul(E zWKpY6T?hCi8SU?4+H`!=>}#m+Hh&ZeMYg%39U6XTReyI`50=)3C#q~=_}wUFIiX%U z5!Kq-Gu{K%%ujVs>Dym8M9sq=`R;`2?7UZw+N>V@qtstxpd7a|z5fR?Jp&PbR8kdQ z@JyVgl2sZp)jQl=Q&2v%Y1N+zwTx&!8dI$~tG{2fadDtMQIG6VOR=5NYTcmZt6XRb zNDi%=aew@*fOx0bWUgjl1^YbuIZ0aUF)Ist^>vD<n8hu^4ji-FV<A?ZX%HvW<cO+m zHZ8Q*ng0C~2|;3X9DUtthxGk|hEg&SO{|TOEl}}}&&gCSTwglS9=2`dRF=h_H%&KJ zCRHPJru&+e?e|RW$2&sm`cXdTCbfo+z=Eg=%YSC2;y>~oD}X9|<Ib0NUQOk-1NPSH z<vGWizsD(NDqXXhTk>XdRYF`giaw7@+pRf>i%q|Yw!u8+V_<fY;N1YFi_JrZ#4y^) zbEX!Y$6Hz6&U&^aY666wQGi3fY0A9qHCKC_He04n9HtUUfXv%E5ZtF<$FycURm1?C zYk$;umt!VKmM4GO7oLgT$47tUQ*qfTPi$c9aPV8CjT}x}$BZJgc)GIodk6VjO+B3? z9BWY}Rrir6Q^NeUx_hDDpa*&tw3v*VIfz(6)%d)vXp&gKchR?+zkVw*d14Nf(c76P zgEgD?S<k&@?4&0h%cFQXwHvdCacT??Cx5snYLKbEoc)@1u92|Fhi4NtIW;Sp{NcLJ zooW2@SG!HRZeq!$^H>iWKfhyL>HNw$3DQ?$3&{_W#$m$t!*Mh}lSwU2!%kJ`#(u_j z=V;Kh{emY`m4Lo23Ezl@-%NH7GhGbb68;qTTo^<@EKerGn#Q2h(b*&1$F->7O@C4H z?LGN>kfCzk@?nRc>%&QQ&DXlG$P6>3ddlSMd}QSfm)^>LUrG#H9W(5cPYGgw{W5fG zBeUL_G#`c*jo9k5-Svyf$I!%sn=Uiz`;WtJN=O*t4`7{N$rMr}ad6s*(Dp;gHzO8J zvTd*$73^jcwu+sVuV>s$&?p5J^ncU&QS87!N3{%0r1eb8rk?TmO;b#V2@B&giL%=Q z@Ur;Js3L&bZdY$R7jJ4j_IkZHT-v59F!Vz#RFT@4Y=s60a%7%xu2P-%C|CP+pNIqo zj`e8dPb3Sp*!qNKv$RV*n_-)6ibvfEX&SR`&#;p*uGLi=i8(waXZeGKe}4ezkJv$e z?rljUxl>?dnt6V8E0n7JgE<c`-J(l8ZPbh6dWKI0r4BzBKXR%GQIZ;mUd1HIppKLy z_gZAvl!0XRtWiUj83E!`pgv6K+0Cocl<-CFIOO3$CM?FTsSN;BipteU+ih=mmh@`d ztzQEbPxs99$E@9H*HjNhUw@2Ek6IT?y%^*=hc&#iz=jW<=eG<Q2VPRoG(eK7w`ZKC zYakD=)N1nV5FQ>?1KN?(4@9ceGmSLKkLQ2QKiMvvsB@JMUQ?3_2w#j~pD_~~_u6pF zSaZ$QXt^X>M&Z&)gnEpN_8x|`Q*PA%5;(UG0}Vf^TVki`NM~1?aeuTwC6|x9q6wK` zmy~NyPCX-+f4L@YLLZDkgmvwVDJImXvd7H=B;#Gv!H;rj-!}MvL{cc(?^-6CI$4$~ z>e+rkOvVRk>nDLG1eCNkgQF<ME!<apJ}oqbjeK`MBiMl^g&sX^QXGgJpqS#3qH4-# zfBf!KzwuOLL3D`O#eZQ1pmD4AdK7fgC_W9oT8<`kleIYN7*=!<WCGjxF4}~MdD7C` z;SqVTv#YKHMPeyGpb~uE_K~vxV@IB6mqgNJ#Sm!XlUW*I&XOlEf9R_GW1)jX`3ZQ| zRy@_nfqfF>U~_qzZ}cxM1QOTPrJq&aU5j!q&$+JZzJlu18h;tgI|04&`<z-W;p0sh zOQ2CRau|?f0Odj2`U|6C)og3mfDRSTz>OMZZV@qYNUZ~4&UpbdW;V&sXm=90<w1-a z{#+<I;#e!|*B0QqYH2|1<lg=xDhu^rb#2@UVkPX>TkI69(M%y`8X<ZfF>2shb3~ji zFWRe%&p^u|%YWBko9MawAz-zp)Z+Kt6W)S7|J6Ih+47&z57UB6wF6H2#x}(o&9-r} z(D%OkpQTSXorsH@3WS1(njG0wL(hHo%AIE~J`kPG<PByCB*aS;*y`~DGzN0k<3W%% zfz(#UvebZy9YFn6?^?`7xf-DyaaA!_T|XPEVdxxq+J8tp%~_jbMH`5BAT;bJHh2|O zU-WTrdtM(932ks?=xT=tfyWb3jz~6eI=NW5hAbGN9c(lfoC43gudT2;{v$Y+D{<m9 zFtUQS;A*0JBH(f@waU|I#^U-g3Rb*fo51_a1@ey%(^jH560)?cKv*-mU}xr(VS<Al zTaT{&+JAR1`a;B(IvzqNZ9ooDs{B>`YXLeKD0b(vmRDq3cO!6JCY~YU<UqDLrDh|H z%YY{X-SLyP!Ah%N5~;r4)kf}nSuZQLk*q%xxQB(F;pd>ozwZ-HX!k1MeVUNjLKG%U z1B#)J_TBEx+>1;shh7{vXQVj%9p)MEoVkNK#($LZI@Oj2S~XvmmV7jDy-1%sYEy4K zXwpx5Cev21(#1v`-PHCynW87?E5#T|sRl2I&zFI1iz}{;F?PbkCAF7e|E~G!hMAIN zL-U1z>`{!zE%W(NwPhWO3wYEzb}}h;5_6E3TBW4{omSKI*;b(4txD)PE~2p71iyG^ zfq&|q^qk@Us)S;ldk$i7h%*ME9wEk&;~@?^o6dV<QGeJWmz0e=CyDA)s81>`#H=~O zakY};=oBCERAGqV_IRj9aLo1s>kO&te-phuEj^*vcW76GC8DN|OHZam$5ROHWHkoO zq3)E8;uz6>w<h=#=O?HhdO~?O{Vzc}KYtf}n2v)}l+m?Dv)~crD)MJ9{jcOqx`B>> zY^oeL>ty~%Rz8H8mknL4e$%^uf8OIrUuF8@ZL@FniqrUq?e`y4O#-4FHy=ICQDJUL zxLvYF<x|0CdCw|@`#|Swo}d7UYD<YkWR;`L+sVQULG9_B!uNVjq-@E#Y}e;&0)JK2 zhOZvCIIgq$wN09}MJ)A)M>qsG1KbTQ+ur?=C?(tfQn*hMnGzJ_5E$?BaBdy4h*6V0 zT*lCEGGLh|JGnV80*-G0jg)Sy+ZwNQ#z47w=_}Bpz!Dm_20~UnCK8%0dv{5SA@O7> zaVaHZgvC(9^1o#x!z%`=A-$>h{C{gfp>d3DMEvj<7p-QIO6qvrf9pueVdM~<NL=SA zx7(Qv_`fxzzv;u$!@v<f+-mziawbRP&v&zz-F(!?tKpp1eyN@16kM%?66=>v&;04G zOpB~Ofb23It=AV!XDd-9HE$vwW9t66E+Ja{_L^y@{%EZknCC^5^9803#DDQqigl7| z8cR8`CGm7&AN6qAA<6{x{t9rX991<r|4Ml(_Bw)TlXzG<@j>KDM75u^zg<9F2q;)9 zm(VuwFK;d|NT$2KcViz$((aKA^7~s5Hd49mz{g5nsWt9?9lqA;K$SF0aR03Dd(s?( zoHJ1;_uXKj2cH&UXW^<GWq+!_w|sVj$N)9r;q|z<Pw8q2ES%O_pMB;I2w1|VgY%Rz zt4)i+t-A2~<96uCn?k<7QC?(@Ht6{$4;&^1*M#gs%Ig7Hf~A_!!QDFSf=6GE9(1_g zrm*x$6;mCvuuo(PzuZC=Le@;!235UDffaU8D<<m(M&?~ke6Ydw34hQw@lKW`fGA*P zeChtKoq{?9+gdg7S|{mK%?WX=d*kw`1w>BRJJ^YTcrABxjMOkGw0=u$Z{*yFcx%V= zm#aLHVpSnf!@A!R^<_f*)(hOV^)zy2qD#f-w(tYkljb+UUnVCYDg>HD_k6Nuz)<g9 zim<b{AxXS!Q|i-K1%IQQ_hu)ia*{Y}w{3s*;L6i8w1RNn48GL?<GKT0on+5!jbbxI zJ6aCt#PC6skk9>xY67Sfq4dDa?*|os%aZh-%N<P#(jBt?$TjY}pF}86g2W!qk4ulv z9|P=Ij~&YkkW4#^<yh^4%Ll}ihA|b2mZ`veza5|+N<BerjDK&wC-RT%z?3qf6lhGh zE_Er5nAXv8t{pJmDZBR83NaG5_|5y=$U%IzaXcWmrN3k%qnSU@J|=1hQVTCFtqFl> z5#}|LosTt&Dy37Wt$@+5y84x-FTsH=MsCD+jpq)qZBM6oo1{I5zYvn~ngjZQovz%q zZT1|C?Y;qr7=ILl!DXo$^cpt=U4#E5@YfEN=VbY87fOpyahnMFUSI?Xrw*DsojkcS zJ<y+lh<>*BLuc=k2mI=EgaYj?}Pm{DQ?vQ(H?l8ds8NwqPWKenGQaX0Tve?sNbz z(wHU=>)E!toqhUX+RA!SD86lGM?WW`5Uf}2NyNc+c7M+os-&;C#{02i4)NFV)wM15 zCe6f;#6u&KDt*zb80tAfW$V?KX5uiZ!IWF{<wRUSH`iBEBg7@GwC;py+C^zvnW#pp zLv2yA{fXLobv^Lx`(?LJEMMgR`xgMUTanF-XR%ooVUbSkIJNE{mE#L#efE=-4xIPS z`q9t>n}0t=8Gqh&m{qIWh4CrJn?5{_o8b?<@FfiDwK}fB7i_Orx#OmEcCQqY*bGa5 z5vEX-h{7xNi@C;*t7YOf8W%O?*qRYr#A@PP2)fj$sna7qiwRQDPKUE%VxcFpIr&x} zU(~ev*N=*ohBdx{xzca%fj>Q-FlIkuv;5>Ab$@w!%T8E}r3UT+D^g>EBN562AH0T5 zgeFr9rOlh8xo3`Ot7gVc!2e+M5AkqxDvua{EG@oQ0p=;KfdKUZ=ffT2rC)Q$5{r~= z=h1TxQiZ1CCdQCfACx)~_*kQAEp@ZZq}~axHCy}PvUxypNc(I}^Ga?@KX3|8nKy5| zB!5+-c;TZBY`q$C8zO8WyBGdA%yszq1`q_LKWOqze}4ezx@K4Tvx77Fg-eAG1?+94 z5I#^K7e~oJJ}oh_l_)M$igj&DDeW}Ac4jiLX+Z2{R$$z;LaSfF5HHwqytK(yV|%^k zByOjoRVSt83B~+L4y$;BL)_&->OYN5@_&^{9k+W9=~?i<RrMwE=qvG{XblCFy?4t> zuPx(>_mVB}{lEOBea8g8jK2Db4*=e2t^|BIlkLfTV<N8WRZ*`P6B9&-ydj}YvT;-p z9lTE|&F9BU5t7LiCqix-PsQ-=QtUAo`D3OZv}YBj?O_JWB8$J^rM^_@5?uF7M1Oa3 z-F}qnhq^loFhu%#81Y8EZg~q&V0lia>%th{MiJ?L-uaU(q^Wb=z1xLa{zZ-(8;}{$ z;M}HU6&_XB^4(pvS+IaC$5Kq;mD%U!Xuh79A^Z5h%>cMVE?~vvkIzRW$Pnvexvpa5 za<(aF@Pn4-Jlj=kSn~K;=zqPF)qjI7Ew4`QeR7Xs7>^5!_wfPjH1wQ06t|)(wfR=o zmg6weE)_=`H7)tycc(#Mp{D2z@5n;@aQbP8jPQ=2Y2sR^R!aJlA>lfjMBMUfDDFe+ z7B<B(?H&5~UD~^IhMoH-%2o6uS6{#hD~#+6jU5aLOFCY?NJIufx!AOWjDHe-le$M1 zzg=0;H*P(E>SM}E6R(x)`Ga7pZ7>TYS2&Z9c*;0b!RffrqHwDPEiOA;oW!{8TpnbK zZNDEW?}Q2>S#mg%aP*=;`;`!1d)}jiE6-J}xiZcnRhS{UPW>!>QHIZv&!isuDxPP; z{bMHBA<$%YHhWc;=eF187Jq~UP@f;L_aV*#Sh?`WTWD}-Gfe^*r%|m@alomBTV##$ zbu{)bo)EV)(|$H4<~(QK=pj6l^Vo#t)=0Lz1ia_%cB(f)a&B3Fl>1c5y=q*{*|{o* za!n=Tvu)C*#C5bRxbpmt?^Vv^c5G5<t)6f$43P%D+_-FSiF^4(uYVD~|4H>nTMuIh z#v&Ucq#)MnA`RUo$M(^%>d9;sy{WNX%8Y&(M^c&~)cu0vwloGhn5)bjO$`oa?HP?O z$r>JvCNWl$J9^TKD$1Jg2T|}HsZCgQPVkFfLv8s(%3_&W`r@jYJ8*MD#Nr?Ol!h*5 zfJtPa*FSqHFdLqGL4SfD+=;Z1*Y@Uele@Rcqjpcdywv<7L-MTn?>^KA|H$bl*t7Am zcx&1Fw;_XPFQP$T1S0BHWuMND=z;z9<4Y%<r|pUEmeJ*356CWNJg8_Bwe4uIz2*i6 zzMuP*{3j;)s9;?b-*3_}Ju}nQsJzQyB-y`dtxhTa1`sW<RewjdY{9fxbmr!G-Y+z> z@WEpIdOx6XYYs8p)QQ0C1r`hhV9`X#S~8_$)$ix`$nE2tS!I!s&V##+_3D@frY+kz zZgF=#tb|iI$gGe{2X;*7ks_2kxhv!H_mM-S_r<uPgP#`G;Z1_S(`bH|Q(Vs*^@Nz% zx8zR~k7}H1fqydK4yQ)P9NyN!ZCYQ%6#Ml89=g+M0NE0y%eS|M=(P0CZ(5JvX5Snd zBSqsTt`6Fx!rSA|gn1xWU)6m(8X)u02HW$ybV)+x9al3&cSn^W1%hAg-gTxxHVUdJ z`B|(?ber2_;gXK6Ps}bqvHES33o2raQ*;`wJSzQ50)Mibb4BXL=*K{bl6)U<?|JPX zPIwtY*yR8~Lz+E|xksE)d=cXvhq}|dJa)drKFHFl0t_}?SMZN@{S&ex1IYNydxtDr z;n6RIaHo;Kn9#hAMw*x@s|b6${u`RV%XD6(RZdC7(*m`Wd$lT{rSQR9IZjMBeXQ2_ zSQP*EHGdA%Npmsj#evp?mAKT$AHdguWi&Zk#Vu3TTV595hAP3=xVY^+PrsRf4GrTe zdOXM~@FZ@py6-82@2l^oh2aA0ukSr6kU97nt4U~eZs`T-a#KIQ{pLH`clqlf8eAO1 znNxEjbNb7_LLC#y%2}fu@hyByXG$Y&-qz%Det(Owr*geE39qId-cZ5ltwv@)ske~I ztXF}q2H%RBUn~CWYNI^!<)cn{Tykhz<=Rx<4yUjCfDbXRMuqtK^0x2m9mJ2V+pFNC zU;g$4TRXA1DLu|*o#SS$Xk>Uza0|Mt6zphMSvn7GnZuv@5HTl{Q?!2{1O}+Z_^%gz zdw-#Nr`oBq(U`aT#MrMS#pCDn+Rj(m^HTn4QCjCpX?TKcR)lklcVNW+^YVp6*@>RJ z{5-`K(+}yirzILcufA<mQ_@|FtDZ)c>e8LRSdX*axylZ$tL(}g@z|aXgI2GfC*LmD zc#d+GO}9W~>h(s0o?t=)7qd_8CIcTP!+#IN{1$QzT1NJR@7rFgFskG(#Z&nR-=eH) zdLioK_RgxN<SOiKR@KNd&*_Tt+|tVnHoLE`!sIWyv@l+`d&XQDSZ=|7nnQU#I~(Z} z<RsqNt0+wW9+0NLbG8`&`mGYv$;OB~3+dTs-T)UGc3GOKw;LGb#ux8|cPeM5+<%>- zBX4q+<aa-$3YM_R<B(fZNoE4bao6YJ4sCg_WWO)J*lw#fpr>BNZ}TliXYiO^Zn9x$ z#Y+Tfi~~E;<wkuhUAWbZ4e|R*U-R6lR;au-prir%wjgptR;9I@kCHmo-12y=Qk)Wf zW$%tYWVzcevm@QCK^hRcc8pLnD1UXN^a4t(f(i~qj|8L84;dl%#d@&FmFw?nzKa0= zZ9!H&j1%x;kw`2t)OhkQDS&3C@<I4Pd+Dd`=urz~(HoP}`&8v@?Kea*+{|K*zxH{q zbLa6)-vdvlJ`4$3RK0yRp6TOsPoVp;XP5U=zvcL+B?>0ldau+HMA~!J5`P|yAzeq| z4!`Eg@3?I)FSgj6y6fY30!>U)$#7t0(oU9Y2Y#qv_3?-oDY$dss5P`!Iazw*{^wi` zE;rGtwlI(j6H;!SwckuJ9hi#)fD{Eaq+Tw%-NjqhSsQAxWL?Q$+s!Xz9;2(ikbn(E z%B_bmPhQPc*;SggwWxM{;(yb(b#khq5E5$E6)$M`Zvkw(!m)a*w6!PXZ>yxdj3V@% zZdOXb(xmZRji?s%V$%jSHdQu2c-t6ytX~P2s<9D!p0XKnbVsa`9HxYLL5m&P#!QPj zOboibJ`8(oeAi;2$(vftb9lDp{DIH<`tI+Zfk$?rm%j;)-n!RjNq-zph30tGcuI{5 zc;>dS{~=gAEnCgc=w5AgX>}0np>Zx42J-zSnEl$OUr~7x&E|G{s+&>T`qNa|=h54K zzCt>j?HW6geGOf5wOlGmf8+?FjN(!;4-A*s6q%kCuyHYJKGQ(nL#UX2IZ|--8%cP& zvj(NeKYiB85@}%+bbnbaW#>CeW{#ZS(KS$#Sk}wHetW8!&*Q#~aLrOBh@F`#u?l<v ze{@ZMeg>o;-d<I7Fd>f=q>tk0dHxS+>sXa9+U>9>Lyfk!X+$^i8S2`AnX66U)iBfE z^`A@h&jP}MhI&olsWZ4j!|%i<)cCVXzTb)?v3SE^Csx|HJb$FsEi5W7*LD)ez0+J# z=&?40FUs~Vm95Mfg}VR6G?kM<j%(<MmxX!ymsgh)m3}|{q0?S=_$JfV<L@gsi|`)( zca&H&<Tv8!i2alX#qK!WMsFHu?9sG}{7<PEP5l=`q7+pPq2L46>>ZN-L&sYO#npUU z!x1zD2oNM_(0|~AyCk?1g1ZHGh5-gma39<yxD4(L7Cg8P?(S}bzPb0-_tpE{_xGII z|DCS0cXzL~_v-3hN4P+#(9YwKlY-wcC(zV&hS#uqM3(dDwd<ID)0v5&gI<MQ(}}F- zS6-Z0`-bb0W2w)Dh%47e<>|RHPwDwC-huDkJZpLGW`ETx*Tt>)*Ygz>)o0Z<(|Nh; z!GVLj6Xf>~HM&RC7fD)@Ho4xqDUVw&)3%|%-6mQvY<IGJS5oaC?V#|g)^@MaK3fFu zb1$(@yOA=tfvM&&v7Lj^8TUccd4p}~Kox<kW`I^G64y_ZS_l$PFwIo1Jwl4*WE zuAdm^xPJtT{nA3a{pcaAgSCRCVOV+5VTt0-FY>@g$2@vo4CYTlUVp{YBgN2;gNs5g znC)U}js^kDZ#0+J*;qqu^-&;BOFF@1GOYf)^Pmdvb>)R48^cFx&y$Mddce|UT7BUo zK^m3+m9d?RZAICNuE`bf%2>RS_7P<~OQvwa?|+Ixk~jeu<;@tByXLEgR<8F}59|F% zUqz*SZ{0%cir4&f!{FHcN=03(X1aqfNH*c|DNcF5$$_*UGQ^}J=%(FpdX2vyf2VRi zv@d0|Usf|;Ep@-1#5><a(Kc%z(@V+qrX}xOez0#Y5;|bp4@o=9K?d5j7{%U)x+-4R zC4a#V4b`9W4chLsLZi$?YV!M2Vf(fkJtCeu4|fQg-g)KcWAMUjgCvc)oerBL<NK8S ziR~QW%gF1B%oP)!)`8`jAGa!<?(S;!(>udsnXN9>-byHUo`UsrgimWfrmMqpzyyT1 zR}aNJ&j$in5j?XMO1Rz}=1}W`S#DvHqknO8t2V`o+QN23-eWrm<H#mO$QKZ|L)c&A zH`*lY(?<6OSwi$gVJ)dmD`N5$IVQl?S1~E|&`0*{gA|b(_p6|nt@hJgm{E_S+K%CK zwW+c^vKALH<Q$Enmtz|mES~Rc%txn_1~)NYWA=!;{XDbsGDqbn_@sa5?5yX{=zlbL z)wha)<-PYFpU7f}T_hp~uB2$-L^=v!9i8+{iQh3k798@%{!|{^B>I)e%4yFfGxOAh zZGr$QEnugop&$s^F@8FB@7|kWgNVDc&-ObaAG40c-Q{1<W(GH%ScP?*_nfia^kZv= zl=m2Qg)(iqFD<%}E`4etvi;Q<<bR&hwu5LoF~jdFBpRB`Vk~Ema?1Q$U<Nw!4=9qf z-%8DkwQv&Y;VhZ{njCaqISX-8=n}$O!veNw{U)Ez$##;cIf}2BEo&PofV{6K7q$ho zUzWU;Il(qPlooMrCg?-d7RwzDFac^lWxDrHJ46e=^w!?8+;k#1t=2!^hksTC4+GMc z80Oz$H{_D*)-`EOeI?|%_Hty{d>1xcoI&Z|%s~ZJyxF#EcJVR{7eXJIu#YF$A0qoS zNhM!6_`D2-hz8ewiLOt!EmN3c$BBX%k@B-k9Rd|Oj(=ShQUZqDoCL9g+(@w<Ja2?W zZ%m&~*=~NWg`GM7u;`8CTYqv~CyxH}kY4VQcahJB4a~Yj){(eSc?%R09`Y^>zYh=` znjHJYR9Dse(JLWBMR{O$ylZ3`W3=58NU|;W3#7jqUguDX(LWnMCHGa$pg{2r31uW< zFlZCqu1gjVPmysnD{bkP4-C`KEzdeR-j6n)vDA+`i$0PDN`W?rHh<##RXfKEabVmI zej>5;mg2)nb|T+2<UjA&2i#HX<e_y`;sn`s*e%pAgyHYV<<ho$cd!VOi2I6F{#X*` zg^@KRO)f5Ucy~JW+%8TAxm#@ae+E1|&GQNW*Y+}xeX~0`Te{&o_T)SJJD=bxUV0P& z%7xIMI8jnWWb}(t1%GdxU><^&Kpx!tFvW@QnYz64!mJFu5^|3UGchIo6HiN}*sOxJ z!KtE!)2@5|Z*;zr-mm}wn0?zH9-X`v58nw#ktd8h&P|b)nA)Ggeoyi(?>&=xyY>a= zwhp7(jvS`cd-cCssJBsO(^6g<AF~+k_Ibflq4WV80GB>zy?@pvwtJv??UbzCDip=0 zN^wcUw(6sW($Qp<!2UvEV2T%U>QZ#5cR#)2GN1L-AX+y!V_=7=?=aiY?%L^dt!r4l zMf~Qgx#WU<*mdN|eVzP~;qyLv+Nq!$-~RQe=re0G*uJg~#8E(2qwKSxVyadOj~SOv zw5AZa`r*BkWq(jLk1G4k<k|gR`_ArdCypg<RMty|G*i*W>mN(!xtd3sp^Ur@<}Ht# z1#=}9ar&mswsSLvRec0+e65UBMys`%4wr-aZ1<8VFwX#q8F{|xY=emg4Sgeb)6|p^ zxgh2K&%Tm2HN&>HwT6w&6Ky|SKEudqX(tb-!8S(+(tl>J8PR6*zP?%*E~*2lx0nz9 z(Zr3AuVY3`yBXBq?_Yg1UVcyjx6_M1?nEjf8Lp~|9LU{wqCPK)Ts)D8&0W~v<J}+D zqUa(DvnmZAE>RDe0x}y%p6;T;)vk$a&~eY`^+OLK9pq!V&SDvgZ6~}(yynjdtrlS- zWBqD48-E<nG{@*?zb+d@9!2iI(Y04BI@M1Hz|i)x4eM@%^CDdIs@*y(0v~#(Dr$p8 zZz!S92hE3^zIXE1Rqx5EFLC<l_O6xJR45-r$4|BI=WZS-oa<rG=V0y-MFtZ0oox8^ zLe^vI<2bgD_27Vtb3Y%;)zf;1=GD$ibHgT(m4EwQ-QM*cIPD0g;HjshVxlb6*niv% zihZ2J&TLr0sFFK$a7bWltmdx0_*Qq3dhi{nft;S))O}p!JI{*Pr>5M7gC-BLIs5>Q ze&!*c1qG$FuI5yaCO(lI)evU0%p~@M3kFtV8GR%5Jvj?8qvoXBpF->=CKvnT4%>QC zp?`f!wFm7tE#9ngcMc921OsShk?E$Qu<{=Jwgc{ag&%SDevg~I+PL+R)d24eZs?=4 z>5qq-1dOusYT1XlS{oU|Iisx1`a$waV%=X-@9#RU<`wS42i@kIR`fHF)FSE#%hQdy zW7oX)p4&3xcDRTnqaGZ+?cOcNX5ltJ@_%w@OJ_U8^)36}54I$ePtQD@BJ27u8+gNa z^gN+c18%zn7;Tx<9@b;S=x9wS>o8FLgXhZq&tXHhmBh@}p?XWaSK}O)>h>=6dglV; z8xL_`N=2JKl**QsTW;Omy^LCG%qPYwc3gCEYLktbb2inI!C$*H@1hrFz4(3^x_{;b zYa}NhL3z6l8L;$s*9ne8*;x}J^w0nMf-B;N$bfle@+F2IZq?TadEyz9%7!-bCcAT` zjw?~!tO#(Fsh_2Lw;lhK$mcfZ@BQHw)qPRq*dl$W8q9G{<3m(!sw6XoSw7RmW-^|& zQ59LfPj8k83PY3UYC|xB8MKIO*nbbfIAn_Nc`8i5TWqG<l*fBVqf0t}5GvBYEnjM6 zA?nW36$GAscrWXu#{2a=fnvB?)k#=Du*z<S+$vdd2$3`T5O6T2>2YYjZV8N<Gm?*B zyA>6tz)C<3%g;R|BEQh{C8B%=Z}&GoZRBTzWai(pdz-+J2Wb2WApBg4U4LRokC#{S zXT&5M<W^m|3>?}dMge1@cWcUr`bqjGpqc|A+0*8{J*0~6$D<}V)c@D6f5S=v-yF|? ztb3|qrpFzFenb_6WqwA(I;rR7TA$s(6oV@qo@5jGhk(&(mQozel|!hB<vgV@3nSH@ zP_L4sf?#$M&{hI6Na0eFMt|)HoaD0q1mLx=;1UYL5e;geV;g`BMmX(ZQ`0OgJHcLj z_^6y2<?`q>yq*$-i990-Jg{9#Jfa~>yr!Zkd~M3r`O9%g`94?$_iMv`<5#E%jM+B1 z406{bW<IeEL>nA)CZ!}dYXQu(2zt+NvL)6I^iDsIE~mVZQqMSg?thnZt*)f~{3Afv z7&W{62L|J6m`;Mm0&G<$$-4c{dwROQ{u-hI)sTe3HEJ2{f6*7&GBTnCc%#!$8XJm< zU4M^7w4bCj#ie{teDNi22x!D{llk~C(&TmwKJi>OO|+4tu+pj%f*gbs<JTsHeJpLZ z4Twq1!RY%TjEX&gYk#}~h;bdL-hGV<37OW3AD@^w`0N{ZMXRidBXID|3&ibwa_c%Q zb#c$rXh^|X$b?ga;wi~jXg9q=>b4_tU1}q*$h})_);EUxvL0<(AoNt0yF+bEgE`{T zXZj10frigrcwwrMwWDaSts^y1t{AA0&U}X3(fdW8m|>0an|}h%BtJQzz#1~p`<aS9 z%wbHeV)>T6plskbs;F$=y4gxty%qN5@g&K;M3IGFT`5L*1b3N}gMW~=_hATq38@si zxB(3j{6lcX)r|gva$vkp-`zZ;T9~<yOThH>I&HJCX6R>WgI6Urv&ndf4ebKRnROgO ztC5*fO`Q%W@qg&{R^E%<{cZUP)7C7FVTyTkLr+2>ec)8c@wYrQ4`z!7&G2{M2q0l> z@)@22!fyGP?+TpCYD<ct-Y3T{q?R8vd|;DBd2E7-HZAYwz~2tW`7#m;d#l3DbEa3c z)iuHNqrH#q%TX)BoR~S%4(Xl(SCs>x0G=uQ&Iwjx+kalLW5OM}Y1Uq;5N%}9SV)<W zH_p)r{OoW#PiTWuGa8?HPn;?p5V_SzCrZL=E7#Kw*nb)GX%N@+X2%*l&Jre|0~iD+ zGT<72$Q4sr=PFSyi@0#aFXd0S5P)*Db>>BKY`P4=3jNFUX2GMX$hpN-Q?72qaZinf zmaqF3fPWb5%r#jmxpj%ez2PWYu|)o*@9S0#mYR8VPtCYB?Ybqx=(O`5h~6GANC#Nq z=Uf}wPlq4)w{Mu|?$=;Z4{zHA041zY_kMwh{59|;f+Y+dhc@LqK`8~js#hytwSPec zgF<lw`!ypo_mAW@V1mPK2FrSuDcg#Hc@6fLseegr@Uez-sntRUX|A7qn3L{078S<d zGTZn$4%SKF>YsAx6wb#&ixLXAp?%i81D^VUkpYNOXzfa+>RR3uyoM}R>0rMlRPcBK z%c}nFAdT8AcTETNF*&U@38rIQP@bwZKOPUQDW9NwTyt~_g08^sjE_qpaI7+afk_Gt zeSgXMId?Cl5*vATw+cuUX4m2z#j$ZY`)Yo9Ia|Y7wpD`DFIP)&AFs$OPB#h)#bgQx zecIgK4e@5KqH@{xjg9Vd*;ZBs!+zuymE$Et-zHM;yUSS4K5lJFltR%*mzX9ur4+9= zWfqJ$*<8`oV1wXDeWeksx8y>9W*M!mI)9Q9L25pnQ>*qA;|4ZXJA=zJRf)L(!dSDy z&0cx!QuLqmTCxeei^r@gJ`~OUu|r<$Smkk)t<zU-@<)nVGi|*$Ei>E^5IW-)y4L4{ z`^O`z5#i>aO&~*&Y0r9)zID4)e_ov)T?<UrI6x?3I{Z{njjm(Gt47S#Ug5Nnj(=KW z16sxD>Oa&CUHj7Nlpg23oNvQhs@;sPnc-HiO_xElp_K(suQt#;TRx;KE)(`Ho^MT? zopG{u>XqnORhTorZh!XHMKVH7Lu+?E5j8?GEID!*h%V4osAGFls_(Lxk!p{dA8k7h z?DN!57229nnS@U6nA9Qg8Gxa;w|`pT?5e(as}D`QF<uYo2|G`(C4(j98Kn}RL2OXX z`pwN$Cn$-A<x(AKhTH6>hbW_K?3GB)sAI4h=UeXD3PH}q_(UqV9sBN3jk?t653>~w zeJ65}%ri3<j-b>m5ZSI*kz5|OM_Ex~gLU{msY0&n`=4zvdhfY{nPT@6x_^>{<sS9@ zoOV%xzC9)$9(6YLkMgOIfk!#2@Ly0pF6Exu$nHt$m-tdQ_(y!)zYQ4y3q#Jzp@oA0 zrfbbm0@o5S4+Qt#_soeZB-+=h$|S3}m(I_3t~94g$~C|q$*5%i`qSH}87q?|nTdTm zTbxcEw~?}FXT~0lWP6QYdViPNS`P2-%79c`->>XK;`8OV^+um?%Z*bzPvaZsSs#bX zaT2rt77npU%g|hemEqnb#9Ebq%b*|%mj=O?*>#2Z)A21Z9CJf`C*I@44c>>=c1fEr zbYHUFeyYUQ#EsUbkP=iQ1l9&6IDII-n!2-0CJhdf3S*PiU2~kL+kaXsf|{l`23s{2 z+~{cbt9Ertp28c+_AQ1<FadGJKqUsN{e20WZvg?RBN<R>ePlh<P#bn$Py&Y?D75jS zg0Kn6Ftx5<e0_hP)0A*9e&^P7f4kJC2(7JFL`Nwk9GZuBubaKTW5Oz1#>3xRQ6owd z8I~i|wU{iQoDe*6c7Lh00ZY&0JsPXt?199(UFILsFilWt3eqgHC9#|BTPIl6bwyzJ z5y4x852!(rA>3eYOdB0d?CsbJecQS{<+5!ncHSdLJTI%ZV~{%VRiwUZRxkVfYLjcp zve#9CE@jTm*i$r_SnSvaUs4r)_rQQ0j!78J<ep0ztm{RUf`6sNI}IA%2WyqIvY7M3 zS31Y?+bG%NAjf@0xr_0PyQ$(I;u&|vH7LK{#9#IrGCF)T*_4(_s-7CKm(Zy$sH9sU zHr%jk+rJd6M>3*QKr)1F>u2b<rA<q#M|uY$%Awm<^Vy+WsZ~haI#JV-2WkM&WLm-~ zOil=M(gU`?Z+{!CuE{i187Bna;~-^sS~JP#Jw++UI^u6-fL<6KYiMFl-q{EMRr;$8 zkdFO?@aWbwR@`gFn_zJDqc&~O)vU=J6Hq$SwO@SjQb!Z8A$~vYt52&ZupyD+In`%! zTu3I6c-03_P`d9Lyglt*Y%M3YXP@~}EXDd5h28Xyd4D_VY|SBE*R*M?FF1n6dwk0? z9iz+DR}mJ8O@=`}lTmBvtD(OkUQTkf(|dz`<ZC#S7i5*`@hle-ca$;+DM@mIeV0pO z(Dyp~4w0?L_bFY0&Xlsx6YCg~8M5rT#Ol#3EKtNK1GMccO4|TQx|O<#&hFMu2&M(J z@u;qVaDS00^N_C&?T5tLG=oU{QwE{SyPJatM3z6#jF)otZ?FyGGTU!BqIB<4T`bBs z=;ab|_G-o(m^4fs)|99hkLd8G-BW1pfN*a0S!&|660$3C0l2-2pqMp<l|_eA{J?o{ zI3R`b=9rFih7LZJ|GNZXj^0*^cDI<wP(SQq1%FC+B#KS3LJoa--nH#9<{4qV8!(t$ zmzhhvf{GBFQ_9p|^7#b%Xt|O#bnxwBI*Y9pP+}xgmw3r*u~cwG#Le|KqdOp6RFn21 z*>Wy`R_D+pB}rntsnmER2XP>*-&bfowhN3JX`U)(li|ghbX=krtFxU-)W|3LrK!p# zet&n<Lyq(2BtAa@CYaBv%*9Moc2&CcW&5kA<k$Sc6TxsJhHnOqyMykJiYmH8<N8#? z>C*giaJ}{!0?k|m9il%%BEV7o&vG*LU~-%a<tu<bN=cfgT4?*5bmoqfH`<qO&|tJ2 z(pMkBSkXeYWQ^Y@lf>?^!p?vxJAp2Qlz-`#T5h-`*?sF&HFRsD+AwA~U3z!augQX> zZ5-;(_m4%B9tokJRR+~b?9I;$Tk2GMv|qinYbI7EWV*nmYo#Qgew_2@9lK*P#9x;T z8<S4@p&_4ncUWSa$kuC82#uPu@+E#5Oq#|9ZsRrUZ`%G8+B#i0iYTNK@8pVveSZeQ zTNDH@A!wt_Ysm$05%x6!k9@Gm0pppAi*;x@OklwnUPq}As2f#f9Vko1Qm&#;8-=4I zpFG`F63&ri3N0r9Ua6i`CUafJSCw2<-gO4ZdPfwMvIDpFH8j;{t%__el68+Pk8j7? z;4tgfyijve_i6PCQkCvMM3IH}0)Mka<*_3rP+4fp6}25VV0>|Y+}(@I{J6Osi!w{2 zvT3M}_4qHLCQQmNxV&)|W&<e?rdD{bZJBo3+t0l!)Zt5ta+@y;be2gigO6hy#!<}> zrlm6T_|dK{%cW@tYH_9q({|PO5F!#KtGiwU2YPEhLv>3gi5i7LxDxQ~hkrio=)`<p z?bbiFUQ7ytl92!%V}TI$1Nnmz-i4_1-d>{lBmtSkiAXw(i`)Xl?y$TsTkU^kfYjip z-Mt@EUc)b(r=`1GzV1eYusKu0D+*K#jfn?mf$Sog#u|d99Nfyo3$0`t$=g<Nb%z3v zLzW@y;Au_YU9Y-aCGuLuR(~1*NJC#=54qe^&<hTacxmCQbyYQQ>}02*f7O}9gKU0W z1YM@6jT|<%XEm}Q@vakJnb78?gOWWZ>w@a$XI;Fw$OPN=_f1$qw&cJ)&QAbn`SDPi zut-!))8C0%aGrWi)vcwvL~MQD5VxD9mxq!a50zEf25Jv29B>zJh<{hi5p(-cmeZEY z?wMTtNMo)iln)QDR6X3KzaVdg4K5tYaH<^9$d}#S#5It7TE!w0p}(=M3a=K{B<CB9 z_^gO|c(7TQznD$kv&}2E0`pw3tI8iZOB|hN;dn{7(S3Hl{jk~mPE{k|bdvCEq9Xp0 zd@IV|ELk{1P$AoQ5`P4OIJWz8-HYt*xP7aT^-iRptm9qRSx>Fq-}f1vZi8<{2o#sT ziwvw8P5DSn{`h^J0S~@TeOHv-;6?ggkj>MqKU!qLcDWi^KG7$XwVKBI_B0z;lIr2J z(n4%T>$2saQoSl%ZS54QUrII_c2fnVZePUEV1)&#WYnM!b$|0>2P`_Y_YzvZS^-@# zRBCJvxyTXsoIyS6NeO3e)sMo>``mMqJl=dCFK`YF+^fswABCsWO7(k_4fM@x-Y^&j zN~2e22OO904jusnOfsuL{JF~wR}9ig+$FscBgK~@zwA^BM<I85xv!YWYOM8iBYO^{ z>&h>e-?6-M=6{y|rPC4Uj6F*Y<jUBDvPl{W1<NiUkABn1JSHj!GI`rhVzQ}OmTp;k zJbG(%#J!nxDbBYlQR_P+nLK^4wq~@lmNWu=ozDZrssk0Xyg^vm*C<!HAUN#<Ed1A# zh)v_SxZ#b67exjW3E`hd>BOAvu8g&hi(U!mwlXA@aDR+>x&kxa%($M+Qv=Y}qiF<j zcw9XkLf|WhjYQPRnv}`w(>_z@bs78d6&|o{s|k&ZbfylC<PnNG2bYv&)|2p1oFnox zPOyymywuiy;|*V<jc%#WG@GzSb}(tGmtt~pH1*JsMf#OfU_Uqx76#GKU&u}Y+XNSk z|K&wlA%9MYHQDB@rheUHTW9Irt&Mam8f6{-c52Kyrl6{()dl+$+{Au=vWQF|Q{CMy z2c&L4U$9|bj)}k1UXHb9228Q>9=aXl(J_@Cbb>-jJ)+2p{inN4-;xpn`?Ut0NM29R zRwc_Lfx*X!hXqzUAFIBJm>1~L|It2QNW#BMzJH?m{GZJ(;zgjLU1^t=KJLet|H*!C z<NZ2?B1(zkkYs-RC;JtN^LIcbhzKhGFJ8vHUcqqq$^K(>OrI(}Z|rTCTjr)Xq+BzF zk7_4%z>(U*xR%<l@bU3)?+z-4v65lAg$A$3t7~jO5(;%zoOk_MP~F_bi7J}?p69BJ z)_?tJw+px}40$Fe4_SG#HEA|hsYj4_fonNgiDphV>-z0lBxwsN>*6pyf!Cc?do4Ay zr&@W1Zjw(qlCxRY)DZ_OIQ$ox9rtx{j?Y`KAEwK!FHmm>JJ}w#%Zt&xQtpTM8(%Tq z*mj9rN_gBPZ^*4IN(`7row|Q~fqjCQbAM#B;5-jwd`F%V(LsGDsKvdPMt{=3_s`WR zi}-A5NiXtrYZ{-BKuJZljuc>M_GfVUN9%d%+Z2*QY~rw~fXeCd`PgBtQ*~OUG@&<V zWw}yiB?O;%qy-WS;*F100l&6ebRBzEKVP(Q3_bS&XE}Fu7TuK~rVN|O8$p4A#(!G_ z34aC$IjuN3IKCTXkh5j|!tqhx@(xN8VZD4|0#WGGZ%`DU`IWso{;-`fJD>vkg_e)o zT(<0PJ8?s?echG6bIESUs@MG1@cm_vJC08Vb4Sd*d-##?2W*A1PWcnS`8%IEoKPEw zdt{z+o#4YqVaeMBbX|3BIlKDb-hZl^$hsiDR2)u61ABJj8fOe=L7{*K^U)q2PXV8c z1FKIUJ$lH%_NgN@gTEGk^{Q~Wvq0cCD{o&kOkh^@D1hYnfgeW5+9QAAtjN{eDoXxv zMf=H)l&hRzRQCS{+^PgxQGJq++AXL>X@^mHq(5l;9qZ@-4_KqOdUVWT3V;7L1vui; z0;bS}mFRh$?oleVSu#`6_?PJ0oc8nz@ACV+uQq`&^~I%HuH-b$#`uURDMN&cw{8a> z8h2#cI4(B~)%79!u<c<1v;82y*R-(<{rEnC!NJt*?BmL1t*FS5S!o^Xr2ad94sFe! z(%Nl$rw$ey8Tt2)2mB050)IU5<g7nggx<)aXKHHOfzby_Y;)KXf9%qK;>n1|a92y% zkO3zIfMP49enI7EhJsFukh=R|8`CfIb~I9|z7<%QRG3*`Q$-7W`?f`^jCtu&M;okS zjv=il_KHfcM{qFyJWc>S?gO0WFH!v@n;Y7xsHo465l%fo^=1BGKz~&n8erI@=~G_o zVFZo6OOmfYPPZ$<r-?c*on8M_@(qW;!Ej4JMI+I2Me>b>a@0Y)P9*-dQllDI0?jKF zLnlEWRkFE{r;hoA8e40->r-f@2@eA_LnOI!YdW-GFa~EG;a}l`2|wr-d^@w~svJk+ zK5fUlmLmgrT<u<MW`7<xiswrR&SxcO&Op~U9n1iVYvo`5j8N!qj=btE6%t&InXZ3i z+WccIBSW6Jt!`R!uebtM)H3B&>Wfy-yJDG<FE+sApcC%HUIn!T@uO%L*W%@{5U%VB zSTzPM&VR^^@bQ;gi#W5$O)?lzUpO6_1fC)0TN|www{9mtet+C)ZQsDRL;Gj4seSp1 zmO~W$aMm~MTp@VUf(P6l;=k6JvFj0)JXI&2q-a=iM2Dpn$6==JwEN9LcUHR$+)S)} z@%Q;Y$(HjQOd?8Vb{xxMxRn#ZP?4Jh()rZzeW5P}vF8daY|jkKr?eWjQJva^QIfHF zkpr+d&yzJe+kc>uZEXZ16<dxggK1Ytgmogjl>ldp|C7t+luMfqQ-aelZ$?kH99{Ov z<+NU@v4}SshK8Mg>jnd#a9Q|}GBI7ZhTbTDj$j{Ev1v7M*&z;`UU|v^soDNUUX2R1 zpKeH@o^zBqM}x%j$KI39uEPV`X+0g&c(oC-zMg3mFn_<Y*C~TD146SV=EEzo0+O8y z3(D2!Mj2?<hF)iqwzHKl8y1~02w3$(u*d|o>Mrh=##DWB>)L-e06Rd$zlz|oG23El zknw$t#9Pzs7Z7^x6gzb0fiF9~J5FQAm!-mfUQZybTs3ON5F7d(e%4NS)46Do)m!pM zYl;kCU8RtO-AR9-oG6ab!s9g<vxb+v+Hb6EWv=m}T4h_vFcGgUnc6Q^0H>1ARKoQ` z<o$QBaqTDv>Mz?aY4^j8;cbn)ei)_zwwnRhVv7I$4CRpT-insNW>JGa5@4oewWl5n zAS+1r#*gaPmshM=PZ$x$$3*$}ZQdB8%uJ4>XgR+&ozs7Io>k}74h1NH%K*o{knd_+ z%)d=fD%`Ai=5WFfj$80wlPm$l@}HDp@Rj8%(c{=O`{B-=v&G);i=gMa-Tfi&3*YCU z8CUFX|8^J>LS&%M?)gQ#=gEs85JQj09yRvQxXn1g;P&zyN!VAfC{zodsLIT^(~F{> zTTQrxkk@}O8{fnZ6?Zk(y~MXNR!TcG6g9g}Ig6}?d@0^VFd2t}Zu~)efK5m!^r8H* z2M`V4bFVmO9Ll(Qg_Y%v;Ukh$WuK8%-6@&(J~muiS$MmoLEq>1ajH|Oc-;7oJIXbS z$LrD3dAt1z-_k0B^}qxU>q8CJrJ%}McD|W<+$?|QIhxLjm?(O9+Ucz7#m^{XdJ#qk zNH;hFz-z}W{mQ62u>Ci_c<k2Hz5CHeQ94jg@zw57lR-J2>~>9t6Okao7D|LT+YdZ; zu7{Sc3AjG|;+0s#`6s2!Um*|(sZ-EN>kSn@{|E7)xBT1t<T_XQ*ZXhcv$XvH!(#7| z=$?O(3hjJ}4O#SAJ1astAQK4dq9IKtz0oyvj;z)d{;aD<Qq~KSlusuLM}g-LVsShN zehW^Dotn3kX>;}$k}1+lUU(?v4=*4rA|C<MKl`%WhlX~J+l7!gSL*s1<Dl<O(oH33 zrA-WDOgjsJ3!H$ApBQ(8-&)QU-L0m*Hu--GC|Jo0hO#jayjq4eE82x#Kl``|v!?rC zE4hsXLwQ<4MSWUOXO`KJS6Vp;`Rx%=DR-O!K6d~Wr04<G%t%zi*RMy@er81#UGqy{ z_BxH7t`#j;POI;*#k_u?`$75}&UVC-LA?bYiNU?g{8Ru`V_o(4tstd1$6C4QqPc$& z+iK*y*B83BV!g}YCKDw|3P^Bh55PWWS~3azExyvnl9!Ja9v)r=+)-L|>LVlKvV12M z<uW3frt&!&y?tL#EO$M9v?_-cNn895gHC(!ZvWMLI_w??`^Fb%)xEbVgd&~c6qr2~ z(py9wNuV#J{R<K6QLVI!W&MM$Tw;F#(hZ@fMdUWiKFC0V(T>#)w;YExJ?dLx_A_go zpsN7!-5RutQiZ-zC0Sf6s|@BI#xGel<-PC?8m}6(IuX4<Wd6dniAIN(BnHAKPAb0r zL@<th2FE+I*zIv_(Vk#&6tMM*%#rhQT4Y67j?)?bBs(iSs<Sg!CX}VO*;jw}$6}Pf zQ=f7H6}K%f!P5<><$hl9Pc(`Aw;t;URU|`U4nTLLYW1ey-CIb%?n@g!Xa)nm*Q4b= zfDPox;k+#h(G>B<>k3*7dmNn3uD(DLI=@ie^bicNTab@gmN^Y{#$l6t6vPxD;WglT zvlcwFu>t`67G2iz4+eY)4vBx1uM#RGPTEfkg6Vk~5QSyt_EL5$H!USl%T<<pygP0L zQ{hrCalrz_v%-D+jQO9td#7Zn4hP<YrTKW;Z;H{(NX1voCuS&1M+0w{dbH_RKJu<W zc#Q(lL{Hj)jIfa2X-+C;y;HdT_`5?&o~C#k3q7|B5K;2iFhq?(_kn){a|g9sI(0s> zGbl%xp7)TgIfA@3Tq>-4F^KU)@Y>onUFhosd@Atl<Vf$yCMx~+QDPy{d!Uf08y-1K z52>cD_ac6ISI82L_1J^17i$k)x5HvvdN=NBXO$RGnyMyucM8OLh~)F68YyM!{B+{& zU#3n?CZ+`L`gOuB7xRB};Uu0y_5ErY;c|DggI9<Gj*(QbTTjU3CzJK)HJQk|wS$?= z&ftso-|>`5MoBYgjGglRfIOXYyr!t~>4QIy0(*uB4H?IvhW@KI9U-6i@wuh6zJKca zee|N<nNL|>epZtO#=B$LUz-j44zP}es2qy!DZ3_0oXy?`K3RW}ovKH5ZAO~5=>+<W zAWFq$O>nkLDI%DHrV(JQlkd)p0KW>H9sNj#$GCT7OkzbPFOyv~h=ZcYLoC4zPy3S& zYs1KCr@>Ixj!O~9MqP_Y<H@p>)CJV~UNANocohSTs%mHvJZ?O~Rnej6@hbpq1G@O# z^nX0c6H?AhW4M3yd(}f*zWUxcJd3%GGBoxop6280Y}mEU^P+cEl4tqW;1?RQ0wq(1 zOA#9HzpN{NX*e{S)tFf-PFD%+97cr5pG4#xjCQ~X39=jLe?GX#NarFFI8V&7fD+*w zzc30nCMfkQ!I|`NrTRAUT0WoTnBnG1?QfW_(*w^aMK*t5%>R&5GDFu`Asu*nq3m<V zux)z`sBa<#+zTV*eTBEgm)9J8_@-1W-SZR<ff5Fkeih^}E>}Iq0iHH9Bty|ySH%R^ zBV6S09nB*LwZTL|^~eghWiaYarY9>ocr#`Lh7$E$V%6m8rOYoG!qxP`Hv^Gu`9j#d zHr^xT-S&T*R{8;;q&u!({8Typ$ef=Ys#tW4t+ed2>lB@vWcrljS|5dpYKIC<KDHFB z=<>rS&c{D(8<MBAxE7(@=jSy&Pb%CRJ7ki}cS5t`sB5?>l~8$74rEo>l<7jE7_gi$ z(V9V@$`61i;$Fl$AZ}HMChZlkozSo>GotDlK<R&QK|uQU#C*bb{`(%3za~-ZpkYRC ziOyILrWakk$x-G@Of`VfjS~BYbp&tCzdLgOrz|0aP0!iP%3j?*3Jpf(bdAt9FV<1J zYf7G$&YM??Eb3%gh@?}#IqjOQ=SRIhE~av4>?`QBc2PS15iKr%wjxQQLcILpbx<C! z_OX8ktzXx-e&30}+@aX65G8STJ1-7fPvF;iC;Hyr*{bz4#R44lG|g0AT#Jm%SH4?1 zENyy$;A*eYIvV&JHeN+AuhEp4<m#5>BGdtEBz4mgX!872&@P?HkFKxgp2YT|RYI%o z<Og$;YB@i*GpB-Na1N=l<GW(3oI14>CSHF%&7UIJT+LvBXe4Tu4+^&EBdO2xRCA~J ztq17m;I}m6IEj#=!8cvaknSlToT6pc2&RV!3p`D&Lg_U2cuy`YDbn}fpp@sMqEPP@ z)+F``^seTx({g#=@d)NV4sAKh^h))&)T1q9*&;0S8cE6{FZKf=rOpX2gNK~Da6o^2 z%ClQ(W{{}&Tg!dWezl6Yr(|<|>EoHu_Jip>&{Isqm(lii?b@diD=HdSNU!NXPX3lG zNPmT40IhuDZ=L-{pGOZh4Gr>{yp>kP67GO^+V(rgdjivi;K9wv4gST{(SnF(B2zPi zT6_Oe%JX|tauW^VpnQc+d}@!d)j@v-v$&>?+3|A@LFMfQ4nw7V%VKJoQ1d7{I{i!C z=VYKoetMrcol+uyX%MJa3#TYs$5vH7{+oVa!eUR8M-eBATU|J2O+g&z$pu^@U047I zF4)#X&Y~2rm^cvrL}?Krrg!r_rTC~r@Pwt;Acd(AlRSVXV@&q3R&LFhN}7KSADhbo z=$Otyqpq6$yvi!D(V2G<lm^z*lY*uA61?vZ=np3SjLYL3Yhme|RGNJeazW-Dm*KF! zS0au5<Z~}?4PTmh__|<4D4B_HSKC%y;sD?UMvD|gbSef)EjzW<=nIdwDIkUBP))sW z4fSpDUG_W?Rtt}iND^Sxt$%+TYz96!d*izl3_qJLjLd^3e3{0dQO@$i3dE4?vf|~C z7DP#pGC;0U=7Jb3t=mSu-Hc~F3ke|&YWCIh=iOoHJ#Yy7{izg>wf#;9$DMy2uHL-F zW1T_hhh*hryu>Ml^jRO53a7gLPs~@;z@m}?+6|ZfZ9i^~R}^Lf-JE})uXDh&Mzk2= z5?Lck#_3x-4@wkcJ(GQxu0$No%{`DKO2P#lQf3_Vq@ac7+@#PQ8EgBfa=p$il&^k> z3lSHDL?G=)S&fIQ_Cpkfv}Z~Ytt)n(KhntIB}@5!ACfL&ef6CE7QG<rA&S%Pkb|HK z0^d&!$uYBKxHoy|f4+ZF)~F3>HkJG=d`z9*91es|y+CEW+}mu7NlC^!%AfZ*($p6` z<a0wCMT)7V%yW5d-o7(ypcU8MbIDQPa5<YbNeXy48zxHl$om~nC9zci(vpv%rV%1T z8xH!7Cbp4&u+Wt+`o3=F?5(%zd9^l{O;S}HF$rBW59816MiYPLLDsl7`mVuIku^ET z%{28Q(w$k*x52Vf6|3U9V;yf&C2o9*`?0F;ysQP(olyC!a$Drwes}mgV1|neiM0Jp z==Y=Mj1k+O>6wesk-p{KFSo<o!<wNW^)S?b6vNZk@8}sBJ4qZ`eu6KyDFp<GVN>iP zZB;4p8^y^&MhkzY!@$~`sVD@5T0x`u&rj|`&T8@$8%vXw(Y5Lv$@i?^U~X4`M5Kpi zTNt4sYNv5i>;&r4lIWkmYYjUI(C)(fQw>8*WZh6v%f#oSf1)AW@iRS}-?lowFbwZc z%O<?GiY==^Sno_Ki$9bgv|z<3N$MdiTqz$S2_wtt^`d_S2evordZ4F}Bj~#nAv9iv ztgz$6V?Pyg$D!@V;f3zal%ac)ZBCvv>et#oHeIlm=Lv43Uq`rRwWq1!HZL2C^o!9K zRG8VDy-GVz+D&~v8^W%$Yi@Ah{ZQzlkxvmk`9M4<sP=VwLW!Ha{o!1m`o&~^)T5;K zmV3rDzk7eW-lo-3kBsNdprgk1q}J{HJrOO+>4w?o{bW+7J^jXNUsbclA_G&$$0O73 zwlSWTs|mdp!5f>1Hp!8mz~VQ*w!}*Ga9xHg%GsnPI8(~gdX{sD$6ZzKrJ~L|O`20o z4<H3q=?xq;BVWHL)08*gC~0dE%x>A?RRxup=!}27{~A0jXcL{V*8+||;hFz#RWj_V zMiq4U<!3j^F6jpzd=kM7awyM&|BX^z%VHZfyYo5xyo%Cyob&+*r(z~tTX)}?x587Y zv5xn3`dPB%DiIq`$m>iROEB7%BN_f(*bPleY1o<OLe(Bl5K8q(d}{G~u~^Yn@ma>> zpkRNTYizQQj@Xav&L9XCaB}crth9!W#&w_GL3EKisVU2y^qxuHH^7CN2G-ARqi*vx zeOU(oFP_sIextRG+@A>(t;nB5A-kJz>!y}=AdJigpWhtNoM2QmelXpNluL*=)I4p% zu`Hu+e79l7XVbJsw#a0*1cz37jnhrhMRtEs^D1$fk?GuWW&W`zGF0q^c29iGnh8a+ zZk%zoMyI;<kd0U*?8WZzZ_rz%vz-1c@Xs>vh~%Q#H>o5zJbP;$I$waj9~^zeugF^- z3+|{s;^^AtogFH>L~aUGSTt|${n~nVeNSp&o;kh&({b3P4NR`p{`gnW<d(A+eCL0H z0G`<KvQmdqFQm6s&=bG+v7=Ug>%sgG)(rvXDU9>s9aY=-dSP;Sa-&(5x|`=G&LFJm zMwB1l0X3K<Y_EJIt}{(+k!@h{>~T(6UIWe|U7RNtrX!lvXM+^{gKqW%`DYKq3u<OL zXI@g1x6aF(FLE7H#^_-#pDq2yiE4jdO~InJT%tBRRt$ZSlPOC0ejV(}JFzE*yc)M< zRs;=E*G0gDo5;_x@I5l$7S4Qb_jw%JkvMl?+~bc~oL88qSAd<rl?d2es~dHDUG;&8 zR{pqp#dnz5ZfXht4X+n<ABUf9bJhBS?RW$(e%fq#b*)lfO3slw6JgQ-T8V!f^Y3rk zgqRDIUT4=9)ilfmxpyFmXH}Gb=T6d=CYeN~fsnbYrRG~1B!Neio3{%6(kaG|!C}z_ z#1(Cq=x_2xU}@b24w90pw3OtmSmLPtxO`;`+pR?`q7f~a)wOJ{KeTlkw`*=<n3^R^ zVEcM=XWx<xT?v0GsNEUiyi<QOU2B2UCkPM@N<JyI)y^;s?uv@dtF<GK4jVExyY4D| zXwrH-LNTV?9!Ss}Ay<>D&)_7@$(yilr6JZ+Ymbphcv#7(nLQ-X);T46YAzvKv+^hH zgjW7cjwSfX0@c_ES#%yDK5Vo?Ff~xR8A>}5EiOgvzi#5wG9cH&oUDHtC|4j`b|RuJ zvE&w9?h2kmOlZu3R1<T`|158X%{Zg}RYSvou5Wj8ZUAp-pHf`Z$j@F8S!ai(M)|Yj zeuw)_=dF;)?WC>+QRtP#ph4gN_b&iL`v75&Jlj1c2iEU>1WPJ3PN(h>xaiM06+EfT zYUZ#tyq#Oz1#pe%5I%oxWe1TF`h(d1&@AKNJ<78_(@YX#JmgBmAOm!T*gAoM-n2PO zve>6GZ?KFEeldvhI!{kc65%xa=bgpFDH85?UWts;o1+E7rlux(mIQS?meo$wEdQz) zS1;im?Y?xWZJB)J1NQr3-WVp}oYjtnfP4&`mm@2q>_QuSl+k~_YgbCJ)>{<g{V<wU zCO|w)f6$GvV~Rs);k$a@vl6HUG#V%s77SLGM8E!%kbwcZ=`nTo1e)1lM9(u|`cJC< z%+WjK-q*-%x=pKy67uE~!?N#qGcHBq+xt0gtX_Htj4)p&nQ0}&?P-v6`x$X$_Fe^R zYb*QPZ1LA4iJgBCfnbi2zk+9HAyY{s9<J-1JMig*_c9WHJfpoG9LL#qgUAly&l`<{ zU4G80@%5|uUcBGQYhem;QGxy8R^vJCa6=R*Ul6uh*fQ(t_%#zeqPBZ(zfPG6XS!A3 zVe`ZxfyYLpi5WgF;3dZ`wScwF%?p}P4qXMEzl*q0jr4yYBsi?%tm2e=AopdRHqagF zo69sdM35D|_Z6A;{H-69-;|XsNP(TE!FJ9fnTZ1A`e8B7S4gF{se856#UsARmeuFk zjJi$(e?+xV$&Kjwm=n1nLWEIjWx1~lN~(R0BYrNLkbwgSzpn6+GCF`Nuy6a^;K%Ny z1K1A&G3kFOmK0B5Ya2sV_gPPb`Vb7cr$}Uq5!Y!&*UMeEPjvkwVW@_;F97YQ5!BvV zyn2Eg--5SQ57`!-3^IuZbpzT(s$}LF<!L6@t7JF*_dD<9(?%ExLr)5C52S+Am&t{Z z{QEG!XfM0Ddk-c>g`D$dk81OYE-Aj25zj$c?PY(GT20}Hp59Jk(Z}dZt=cz`A){vT zpt%Euzj5^O&)D4q8u+WXYS*fbF1s*q?`udTYz|EHHcIl7{441p4I~;7MfB3TcM4A* zSQ)Y+u|+cc9Z1olKQ{YDks0?0T{~#D%YVCZ!YN`tx-Z{a=`W|pWOitu&5qdy3f=sz z)3JZN>+&CJ=5>w9n!}eGgD56?OUYqgKXSiRKRp6fD9K%xXQePBy0obUOyMQ!vo=6W z@d7$hXA(nwr=?L=ws}Zl<Y@Ofq{htFEKTTcaCZKHRCl@0#&>32_C}1vy}xdE12#O{ z)_Z7KGHAAvaZ$k3^v1(CcRrJqg8ZLXe+PfmV`aL$i@wi~pR!Gy8gG+Cr6+t>x?eT$ zM+z4-9GZw?YER4CFwC*i*<&@0S_JFUq{Z>SfA1w64BahV121VmR=!QoN(j-M8Sd&j za8b(m@v=V~OHdb=mQ@(TN-Kodz-N568s;0usn6MV9a<d>!<-AR{1TA^-5p}>T>*cz zBgi9MRYfiY4HYSZjg1egcuGZkb(oU6qs|wVft#k)(pm}v_I!QpGXjze)n}$cR432U z3ayx$w<*-H(G#vhM|R<jVM%E@1w-c_bm-r^>XXaqFXnm86cNz3U41PKoWCE&F7t8n zK;3x<fG_2xr72gvazYQ2R>_$+jOKrXLB4b0pi&pHf?36twNjhCwbzyEiEBSxWW0Ke zwe2+g(sMk%(<Wwry<@0djs6Y%BbCZEI=oTdeC#YJSk?7T59}}q7!s-y;1;ZVEbB`n zYiPo;kxc8O6U*AAcSBZAlYd3Fb`I#$<;;>^iZ@(O9r_w`r$Mb3fzXZ}2|9nbjjEJE zw-Gugp#t-tYnY4I>cnuQd4yd@8h$z|j!&>)75CTpz5zJ8Of<+l>;0<l9uE=Flug?o z3M4Tu#TDShmvt{Azf-WlXHA5uS+gePOWy?<QAmpnT;VKq>CUS$#7b5iU4_gJ;T8Ml zOJLrNYxEH`!A(q$^;|*~rM`cO{qQ;0wEa=NS_o)`STGD-CSX7H4a&LpK1`}+Z47uG zcYFp5s%h)bxGeS;!5jE-I)}w4Eb3Nf-0M2Mz3y!b8*X#X3Uq!fi27hs9DfKw^<@!M z@d%d|{ik>tMH))~J^nigT)yFTu>je?yqByd*?6HXO9LEbThc3Kh%kTk#Bv)VBuU!( z17~-LN(9%;fW;<rB!S;EY^mzsnU*(B+L=O^+^x1r(1aaOd_;1_nH-?4irbFJvD++D zjVyy)J=zy04i_334S#SMAeb|+h8J4uvO}|c`9E*$KHU{K=?UM=89r?XFLP8sq${Ld z7FXld;Re6$4*FKbP7Z${nJR|HFUjXh)|}nD_R&Nk+L&&{;|X!L6n|{#%NXVT>Scq? zJDmq|EGj3r6L<h7<#5*2$IJs^-twL;N{8MEn+ZDJ*9{Lt#__eqRaxobMwP{<qw+#$ z*3M4ukm)}&(oK;jVBrKF$@_h(3iMa*a=NX~AUX_4L|kB1;O&3S;SA0kki*9_GGB!B zqoG5`%K^Wvwo8%B{CB?IAiq-Ek~VrV^^=xIh>fq^Sy1o!#hen8--4U^)6VU^kaiTh z_SMm2Pq?lvdF(&PEl9iSp!7@8^*D9c;TX(3y5_U<k;>>?xbV7X#W>E^Fq2vNa2{{1 z-elAFs#yD_vq68gxcD%&*~YbiVc3rEUA^`>xNA>XDCa(d&@;^+Ikf@2eZ0Z^+pAcG z(bs#DzJ&j3&M<P$UH))*R8t^<p>aJpd=+ZU@g$DcR?CRV?Wi8{U&pgga-Wkzp7yjZ zLCUqC3mkZU7JW(DQ1(wH%fR`^mGZ}3v|98pU%u1%aCm<(8EA83`+~Im*yXuLg&bf& zAmDk)y!Acrb~tN#)LTHR-4oErQkUwt8N{x*K}+-B`X)@N-sLq5ljBiK(f!8(f6<wX zME_u&OV(!{_b=TWNOYgqG>xPpzWiHfg%tYNTey^96#U<Opeg+5toR*4<o~bT8@T_~ zz2Qm<Rs4S!AMcW11RDPQz>e~74JIVCe}g1B;?qd`7at$lUcPC6#cA^P|5*SMq(e~A zN-Iv?!FKM`pYn}zpq(VLHAIjbFZRDH1l7A`SP7m`UH|Z~q?DA@DOWNJ?LR+pD*xNQ zA-B337Y>JqgoTN@xYVxPM!vRNNX&LV7O&JyYV>~-6TFcgg8NEaij@qGUi>1eSbnrA zCH6thyhE^|GxqBGdcDXxDd8OtGPrnmoIKV=-9N8++*4yCwHn%P7)%6tm-#>Cistj$ zjMKGz!f;TL<0lba?h;$f4}sE>enuqjry>8G@o&+eM+*(6J0lrVr)E77;o;)XPmeLF z`|^K|W<5x@{D#@<8TUUtl)cOD#QBSEN8ffE+)#-LxZ2393eEG}%Q9EI{gTHySB|W_ zdeLfnTByzYOMb1e@llzx%KR{nXfNJ*uKtl^)JGpng1bMA{J)W4jqW(NQRS)#S9&-I z-C?^}($gFO0iXT*vOMPyDgKz}x}T;0_uhXFQ&!q93D_>w52Ll2m=L=Z4-5=cnhz7d zfBzn7sK8?nsEA~!k#iR-HdX2~Dqwdz;z<SpMD|`yN@x4y_`ppN7M^UE1$(<WRTFeZ zw(iJ@_DrCHxhQFh=Qnr`t#5>>ts9xEf6_qyE{P8RUkpSQ`!k+C4Ptzqz{yP22N{18 zxy%jUwtLzqjJiaG2p1@PNcmsDkwGt7wV5f`FSuf0U?39p5ej<yVf~9~R}dyWGxOys zP_#<(eXjkplv@Q|Uww-M2?Bq$0>au;B>0S42r7{-)9(Kko{LXe8K^aHibE8Gza!VM zJ1*^HbgAFyAraeMv)<?j{-5@o#J_(a@0;RgZ@jEeb2|7SEK$)ol2ZLD%~d^H$d@b; zpbdGz9ehsaF#rC)X#SD<tDfE=u9wv$d_+@52F>GYA1?8t-fEI=w)--S*P_C<@@$Jm z@9_{{TF9@>XMc27_|A2Rh*6cKf13Qa{otCJ^6Ic(&FSuNN|YBB)w{JAYUO|bCgv3` znq#<4EB-U3jX4m#t(Jg{&Gx&fEwiZKBmBPVb_>A19hT<gqce-GEt&Yg$U8QGemGat zmm7pYAkYY#J71%`WqZYjsa6I6{eAe{Pk(9N8)j#CP}7JYUr%_`l{bhQd-w9VYnJ!g zkQ`MS6CAgZ<y@J?ZP+e8{MLU?erGD3u!L&^r}8xY;HZPVoYty65xnYq@<dRVLs#&s z#?yW|SU1ya%nZ9U`vrN-|4{cYThMQ$`8YnMqwd^eUwUt7Gix==2N<^M^B;^vx%~gZ z{!YzDt#q)E#Cvb`dYye!IwSRV^1rjyJm=4_u!0+LZm)_F+#m3%DI$MQqx~J0wmawn z+a;A6z<1nf;?_ZiPvC9SuMS=hdUBszV;uKTJmZCN^_tVoebv)3JByFs)yc^{oOrNA zpeQFvMu9A^W(wT8{yg7UD3ndT=Li&ny@H4|A@d5kP@`}s)7$*XlKvZ$BHHW$seGV# zC-a(f9Q`F<=-)&wVBCMm^FC?nBm0+=JNT^kdzsy|-547wsi3~5%&#A(k<f+z2ep1b zJ}>v?&bCzlv5{wZI1dZV{QJS9<j<Cts!acfu(yt?Ygx93gS!QH2<{Tx-6cS9hv4oI zJh)4+KyY_=*|@uFa0$NAjeq3cckl1Lb2#H0WBtcswN!Q0oU?zrd$S!T$Hs10*5kD| z{%i!PY3%fBD>01_TP?JicyM=!pJO^L<-#8E5043gr}uTF?D7<nlNA`lD?2MV-Ly9u z^i`g3L}F5MT~5Q;w-hUw^=ix={`?ji%72hH9Vbi75c)_7#soPGuSQxHFbwZ^kkF-P zG#GE}nNIJvH_U%hX|FZ;y}NQdaOqtIw5%y0JZdgFQsg`Ic7OGJWw(Rr_?N$ghm#B; z9i0Fc51TUhMu}*GFx$139}ShdtyrhBWcUNOkQm<%h5*7LUHe%ac2LJ57idWwh?`I2 zLgvW@OxtTS!6*RC7^FroBl{)0jGoVPG7+I@q{+KyH-vuz@E>1J8ReW1wn^b){USO$ zC_d^A%nO@`XoykV`uhyr@Ih6#OOW-_kkoFSx9-BR4cco7oVlhMThE&`365B1;B^5o zu|F4I(aA-AeqSsey#AM-cfeg8FQN0g9;1$-w1%<SZwR4uEu5~k>W1a!=1w&`nJqNm z2A~G!$FP4BpKXraMuj?<m$C^$8NX&7wh$q2d2ZSpZdWq~I#i^QU7;_IJUpg9gNhR5 zxNVuUXMaB86na2X7h4b&`c>!Te1PaTg#2q_tAgzf^`oJq8!?&nc@1CvSx@uj+MA3Z zcm#x=P&Bepg;+H$pVy>cM+`nE?!iS(?1JW?i<*DTfkg<Tm}FcXQqt|P1eJ8!3;PM= zV)U4j`qz{iohDSN*Dj~EHdkF&hY^9ra4{$grEfeCEv~E_A0qfCfGkt_FV_Tc)!dO? zN)x$Ky#L6QKoOr4CQZ5s=fV}FPZbyw_Fo7T9VGOnjnF|iwWA#|rdTk^8YlT*3lI|w z)31LROT))UKt@I;i<j>;1?R{nGfF8chWKjhpTI-mi-MDHNMxC>tx*4BB@PRqB=hKH zGYe(>ZhKF^n-m$M_d0-GDERjOc3c73KYVnTT4B^S-oqtxrf&iuyhj;!@GpFG66&{n z32!}9O30ofPwG!XGHC%yEJ$==$1D=q(wKiYM;~LHJGHD(!8fFZq#M)ywqvMH+Ohs) zOn%ti_H}(a26T!31I~g2m+oP(VVGDw6vt81J13+?u@?K`Rq96<tKz@(y!*nOYQRtV zG+&Lu`sqV(>z_W0bV0oRt#je5xV7@9QuSQ(6|Bz@?E)?^YQAP21FN2b@ee4SH_3mE zX_t9r<bi#`Ken5$xB~mcBA^#E_hzoyvK-eb6R<1dC&K--3@r%+U)Ff1jHg9S&7@k} z^^P|~QrqqZIA4>bSFIuJUqMzK4fr)H+PGjsK)d8^mhVE>0bb@-<8ys|eQ0Q?`TN3# z9B`vhM0pWQx=;Q%y=-vi@lgCyS!;i2MR7+t_Cp^q=rzB2m#XFT(_C4uPA*TgZmWWP zvIiPH+bYalL}L(Ia<~25#R|WqD(<WJgioK0jIv7l%95$7Sl@OFW{sp|awiFMydQ2( z?lFdW?IG9uzoAG%)ImlN7L(T<B&~^b;_g@Sn{NL7XXNy)77e{U&xPfejcR{0fLQ)a z<4fLy{d(QAqgrI+Gw7Yb`tD-rmjiZ`&1g7&xw3k@XWR+KWsB(TkS9pr=BLquZWc42 z#}ED-ik&o{O3KT6Z%}^q+@S2|m><n!S<U;KJY*BQz-fjZ_$bh}$R8Wkbs2f4e~TZx zG#MDU6G#>-c_)`FJfB3!+<$*w47LZbD#G8-L=lz#YwzdKHME=FPtf0$hHS{vwYN6C z^S#>v!$>5)Fc-C9vEM5P^p$D8rWCtz@$qi;s(h@)`VBa*4mZSS^5<HjHt@9dlrVN$ z*_G=C*bd~47Rg@U`Iqe5mKOuH2IJyn$$mak{~Hk!a!6i*)R1Puj`x3bM43xD-zxh< zWjL^mFICd2cWO+y9#0GdCB#Heg07n%Ey$5Grny=Mn(=xBs~$ZTtxEYGJ+`F%x|tM- z_{jdsG&m$T+SzNB)a@>{&U$iiYus0;hzx&k`-WSPeZ>PDTy=zEMO6-K;V1bvz=+zV zBIi;iaex{3&{a8W(=>lD=tfT(0y=-Evr9Am^tFU3dm66xO#fG(U4?{fJldUrpS|uL zLOwOP-QF(exWtt8T%-XcfWbzAp{R1nA_el^pAlCF?#l+IPGrB}?hq^KYs1aG%e9hu zB_0KG2cm!5aULwBIj3|?KYXpdF7H|69c<_IBv=$G2q9`@X<vW$0kQ#A3G5>yI9bis zsFH~Zl(lV5eO(*`>bC2tTqo0e^BKr!>xr&zZM$x&kG7y+L3*bfCAaE^V=on53!J_I zhh%d`iybjj;1O~PZZUffWx?ktUe`Z+K<$i6d6y(Yo5?W98GoZND<3|o5xU=PDxt-C zIE4Uw@Dv%42FiZ{?ZG##ZD2Y2tyBh;Zssz@4GUD9>wfL)+^*KvNJ#3Wix)@9yh$Ri zp~5kj#3BPe=w52&Wd=~;HS7LTokd{tq3?tAqx{Yj{u;iQ-}@&75=lk=-f0}xEbd7Q zEZsQr)nEE%MU)X12Zu-CAI;(#xKdtDU=`OsRc)m^0D^xEY_&;NYcfqczL$~&w@LHU zWy=#05sk88n!CBE_uNKVlhK9OJhufC>B*hEuG9V#It5DeI67q-eK#lHUE<P#z*lYc zyB6FVQvTK(2>+P}`CiGRJ$D-!b17<{1cxNa#plY*0ntk9&OA>uBM}(}T3SmqH1q%< z_D#Pi(|CV>mzkYVayNyBVMJ&BZ=jSSVa$DqUcY)s21ctsl*YLWQcZ=^BI4y>3KjWe zFl=CG@T|Hoh`(XtVWY_^ozI@6B?>QK5ace_+m}7#CoFYX$y*3c_QLmG6{ciVazgfU z*#6)s;H&$n?EoF`r&R6WQBQJTmSAF{6>(QjuX%qf0T_TM6k)vQkmJxVy`F%nS6E|k z?9%WT`jDlImcnsZIJp}m+sJrso($h%8VMi{P!4XnQ{VW`sXpQXi}ifsL+>0M*>cS? z%Jrb9Q?;e;Zr=~To#ycyf=jo)`+dQN`o7sV@FmUPcf{iJ9=rST)fY1vpSV|-F{msR zwK0F8NLuxsaA0tfOJhc($(6{xUE2T9pv{>XrAQFJNIt4&Aqs3UB6z$(MfP?=ZK&D9 zpWv;s7L;j7+K_y~2?6<-Wp@-1ybh;c8ut+3xk((EUiC?nQM%BrmF~=@Hfi$Mj{leG z12ZQlHZCr%O~Z2;*Z+7e0g8X4dON(GM6!S6EjOV`h@JzVCM(jv{^<AJkI=PwprWt8 zeQ8m>4gaYHssB79cZv+SSTATv_eIHA4Dmx5W-Qk3GW_S+exZck1F0<e_n+|fTV@@A zPXLjwc=3VR;0_h0{y}ZqOS*HO0z?YO%{Gx8K%B3fNY$XN64&(|F)2AcButVZ%G!U) zRGJFJorTKF76bLcW6>R^v+g+bUo?$6NpRAfmDoA&z`LL356*CT>Qvy>DgEFqm1w`E z!OLU(C_uwL4)ZK<9m)fdUB3Y@DiQGQ^gI^W`PmO1Yf2y=Quuu=4q4cO;B!v?YFteK zp0%}~D)vd>>CNI|apv+%_?esOc2<8DZ(-L%){!H}-`xBUG>`E!S@k(VnBXi0681(f zK-E5nNl&!9E5FiQite25$^AnCuItRg4;6N8yC=8W$wF9d>gHQlDOcKouA$d>`X|0~ z3u+<_zE^le^cfgFP_l}(>2FCUy^!IK4TgQ&AJfEzke2k%yGM*heVl8z8pVI|&Qt7v zUi9MRWvsMoE)}Ca_N)_Ob*UqI!FA3)s{{M*jIL<&G(8#?q%eHy7cxv*-;`7k{ZngL z=~EoYEM;UN-cOlCV~>7?lYY8~iu7dtfOx+vbk$nyVN&V&g^>0}b#Oe|JC@75X5kRS z?gvvpFhKhuqMNBwG$7+m!RLP@sB;*=KTqGK4v1i686hAuYG)#Q>~Jq)sOoY<u~l<e z0_f6};+;s1woM*gNX|X;)VL~qvhq~|&6Q>MO1*u`C{AeUSNo#CS%9DFLVk3i;MId4 zu%i-|#;lM{_FVz=xhzLg%5F?Y9U$8J8Mfh~XI*_xTbKKF3R)NqgWi9f0<G}Q3qd{% z=F+BFBGY2y&d_xyNK8NW0qDg7fL(BV1<?>$0qKW|hy^XIa_^N=UuJh@5GG>#C`bHX zBsM7jdmi2k4x+vV;<LW4HKXl)F5N#WRC|7TdC+4anLmf9!OP*?;=`Jo&rxYBkAl}D z+~cGyVY`|#hs9@ngRp;|_%>+mjFP8%Um%!MD&H~f$jWwWnL+-52XlHLjEb>#O}Q!u zeoi<3`O<KV!OIl0v+PMI^m-C~oX1~JZTZ=OTLHL2eV+`;Xk~3Gq>6DJnlA0grf4Xp zclSL$MXH}Yv#TKiAQYdxQXnz6%LN7eCfw%I%tb3bVBO>nNC$r~HO^=4fT#Ve+jcR; z{x5eU<p=nN3U?upS`7a6Oaj<dw#L3LuSoS41?LPVY-2Ug;Q&JP{90bWM{VBTo3mnK zuaF~mrMDvV%#AX9zCGxbLJ%p<-l)4wDX>JZuk;AwD14K<EX6Aoe84EZnQyvpuszdy z+>e^5Y9HR+eaU~aq<X&YbxOIjt)Tn0>EZ-jz<!S4{FM<UnfW5;!+7vlGj2SPRh^cK zrd$J#_#pMQ?tiVa^7*W|QS)qgch}n7eAm#UskJKPSonWRSS|BW7hTm|)DI06utkmz ze>cvn&X?ksGAib-2Ixx;=wIw-4`iM3Qdu<q-TAT*mfnBA9S1s87%fwq67K>Bf^+vN z_#x(pr=P<oQa0FaJ^XzcxGKYz{muStx302za{WSV-_|$zOt;dC3-#dmNqOPtud>bu zCMF?i$tDXoOvBUtR+h&@ZHILxyS}Da8+WtxZ5t^k$1enbB???h;7<Ch{w-RX#Jhcj zzf{6Z&Q5=9@#1fJi1hKIjtXN$V8|Xz`YtAJ3=ZUH4C$fs<vSP8;px}Z!p{S#rS<*c z(JTACocd%{@~$j&A#ci&=?}QK-B21UWFCs6PT%3CzdxaZj(srAfQva{U$$zy(Dnb} zF2C5%AF%JE+772E(2B9UY#8wzB$*5@@yq0y4O@Q{k+J>6^s`-h1fbkeW`q{LYW){P zaz2klcV~wK$W@e-xy(a5z5xjAIs8lPe(Z!Cma$Z7>H}cSCnbt^Mt0b;Gj!^kD6<`o zYjQDMakp8hDbH=0i^E!z(rzjnszXYA=`*Y@IOHa5j7ssRI!!Ue0JD713BZ=<wSpyt ztK)wGcWoskB_*ZSu$HKtxX+Kq4Y18^M+ozxya$8mHtwE!4aAF&JEocSpH}o;*j;Ms zs$*EZ*4=*_yC`MQhw#xGrhDy85kY*DyriRtf><TbbD?c@9IiC_YHDO)ec8HT>O?t9 ze#0CX64d<OO;%Ry3z?>ttt}&-uMV91-=cpLm5@-7zQ4}zIOrKByxXI~=g75OyMf5; znmi>){Tm6V6w15Avi`Pup%@nIjnz$Q*rAtq#QQ`5;U~(wnE*(wSU>oV*LXi9!^DIa zW4nt80Ae-PHaHZu3PMjm)9P(A_hY2T@761+K5%$k+f8Kn+d7;So@@@Kc#Ta=Ugm#t z51w&CVWV7C(z??lHO{GO%kEV3zMz)aWCUNZva+UrDePwEdBeSE+Kplt+28lUlz5x6 zA0+GqR}Ig(+vE~*jCQXzMl>_U`7Fgor4e%>pc@?*)ktz}&wtQHTFr#E_B=DexVnSW zYx*q**NRB?>yiDLj|m3*SH8r>Z>fK*YqUkc@XLk3+GaI_g?BUhDPP4$c)?gqGlA6( zrkmWECWX-79`Mn&`qSiUcNK~K)IQ(+T#wQCx|z(Vka_nJ6TI)(e~s8xt(eq`8|`ox z=Ehjw5S2!p1PPGp8!g@7$sL&u6cEly;S(>yu2p08fc}~;HX=G>DBT9AD!G5GpV*?& z@vy#Ce^<t9%MIDu3?atyPv%<cdOj%8ba%X_aX)DIh+0F7g3``~50lvrrVIZ+65jxs zDS$q1xe9?Io?KYq;f8oF&-sr=ZvA3kvEFi>YTvMaK2MOsMLtz|e$-j>G}vZFVx-)b zW^CDpQN!L?pMQw;rLpw=sK<XMzqN~Yy}AFffN-Gxy!6Ko{7#CEni1&D>RY|;-1zY% zE<`sohZE0_sI2gRAr%N{O`nfF5|d{)A|!%ORL@<ui9-xFipU$|KTLAx3**dlQ{b`? zs>H^lLj3e*V$=ZA@o^akg=PK=dgkkz=>^1PJ|O`zs(a%UXN6WTU@U*Vxed<ktjIsr zxs<M3=GT#G>qp_|q`tcDZkHvRPv0OF0_Fq9S26Wg{jNQ{FI-)d7Z+=T*j>r(#!PYo zWax3xZiLepQda`dY5LWg$(ufn`L%K0lv^*S+~}$`p7x(V+mj*%8}|I)6e(myMM4?P z{In3jVrj%_jkSIA;F^Dwow>M&_f^ZxS@BkI369u@jtJkjo*6p%X^&g3ya?6vmL;@G zO{gESyZobmEPH&tY2iQ$4qd0rfZcm@e#qgQQzVTa4P|%vQAb{u*aY|YSre}F1SKrp zOqpnGLLAVs>0pl%7EJzK6vbiTB0sNm%wtG#*sI(SZVj7YJG6heWKI8G%OKBkS{$t& z+M5|A7swUw4aG{~q?%?PZg2C?*0=ibiAV^P_)0uJ7(F2bX{&g)%~3Ft%~zg}$e=HG zdGeM<xDH3hGB99Q%h-6YcTqgTj|i<tyOu5T5&JGJp-IO@k1^V~RCBrA_u+_bsvhBI zm;K-KQ2zsRN&J7MSX|eCtxefTW;b;4kcaz~c2-9&wU1r1=d9t1F*XJ28)X2`l|p3n zz5A31T(4blhuasy5ON$k3;t);J8wk{4yWMdcn%hQR@yrU6%tPi`Tk#d&wrcpQ=QF- zL!k{F-1nOJSf&@R=F}}bePQ9>#9z8AA$7i9I)DCQBfo$8z)Rrpiydr@4ESFtya5Rp zxOjwSYj@vIGaIoEht;?9-}w`k$!e*awHs-T#@ps><A+qk<_^?HbI@()D0Ez>6nj`i znzs7Jaf<uehUKwFGj#KZM|h{BCLZVBVswcdr`Rcb(atyZH-8Qh68d?-yX`;Atc3}E z)4|^@*qVP#ULf1@zR*QPv$4NdRioV$9X|Cl1HqbN{&^`J*4H;njjw5gRJcagSk@nf zbyJ;wJe3cvZ@y;WP*kJck^d&aO_Yhz)4G_Y&+Dx^=iK`u20E*H7c1L7Ym<7P+ydQH z({3A+khXq=qYYz8^~&m%jz1`%d&+Zzq{(iNn=gMGGG=rw;G1I>0+fM`uUIYA-*K(5 zJV}SA8{kB68X0kGWtgFy2p}(RvjkR&#zTG!JY3h~50C??Mg^Vu_~|U|PEDQjV5AWi zenS-XBpkEG{(C8P5<OJ*Wa<<*Vef(SaGkVTwAIX>9tn}PSP?Vam8gGHBJf!HX);+$ zKdOJ|@2bKdDo^Y`z2>>1pNm&twCxw&UwGJAAVC^EbKb<L_q0;_XMFFOud&s0hrL6t ztE8M2U@Ph?PF$F4b>D#Pv9kbxKCFqSTdwuGKyQ8?C~6@U0>yhqqkMnS5<k0DX!%4a z7H1rVpM%dv&{ao-5BW?Z%HvY7@8LqTj*@@aW_?z7<qW&wP4NCrC<FxZTf$(}#9&si ziZUpz%C-)9F^QSt&yl}Avg|$m&V)_=jmgGm5)Lpw<_+c4u^P+|gJKNua-65{`~IRM z0hUr3;s5+rO}roW>Q8*fJJYTowsW^a=DUizB_vooBB0>_SdJj)wop&3tq=_4ePMqG zTAdNSuJw|Uz8b>%DmS@-e07~@Ty@2+!35aA1lvBfrIWbmH>>6+=m+eE-_|8C>I}y! zw~#1&R+3yTQZ>gl>SJy?=s*4WC@m+R9t(dk30*lpPvK2BcW#RoJ?PCM`(!`d;?dER z&Fq0+`66(t&Cfwg6_RBtUzP~P$EknQuD(Uyd6AWFy?)xNEhVGS{SSobGC3D$KxmP+ zJ6+;KQ9MT9RR016fAgDX2}dlre&5z~<*hP27i?f)@CU|v-gp>fv8nlT0sM4+E3A8k z=0P*}QUq8q_<hW7HPcJ&`X;?<$~L2QiAtc6GIRt$YMKNWIt+nvj45xUBqD#9M46{? zYtW}~1L44ToC4=PCOQAOY5d<8tPm=y6u#h;9i7X<KttY>s!I`ktH9!Jqdc5u7%Uuu zux1zS>;hZ;S>8>D2yd?xQ5P9V*+4k=&V-KpTg@Oj`3*1n?b=7IRs@tT%CWPAWh4h- z(XKQ0ZlSj%eon}*5$9H~)c$|LW+HsROeAVsvNu$yDDliE<>h|dPfH(nW=7t#^wzWS z7IVcZQlIFAo!OiXFwf*g_~L%5YF#v45h)f^M|8m83g7_nBgF8r;A=<vdY+gzM7*UW zL<mj$fgQL_QTX8Ns%V8rRRZP@q4Z7eb{8L(hyXTW$5=tVWsp;#60v`wXbQdU{wsI^ zPks0gQi1xIi`ASMH<PvpOXeQN{X*>S`u-O@mD}ImX5%&qo#$z`$Qn*_$W~(`#5Rc> z)}YIKzmvf>S!<858!ceLonA7ZM6Zjmn9jc6UEKhz@P|i7S6z25yrB+1o|oXQW#htH z`L{$0M9A55Z;}(H-K>8R;)qK}ZHjpIm$ay1??j)Yf`*RBd376#@W#<wJWc`QZ|W}w z=ZNn&)nUlVSJ0Hd2P|Nh4V=cqe^ymB*q>)QzgY;$%WPgTW=3>vi2J+aHNn36x5F?$ zKqLg@4nSJijUiq?|2Uh($^QO~b5k05^<q{n;e;AVV>qi78n1u6Qj|{ry5t&jnG`GR z$9IUm89g${uyZ;Bv&@gkhSL$AoZeyTdOnyj5l@v1dv=|Mnx{C+#$0Zm)|F&GPd@y8 z3yc;xX?C{6IXfuzHyCcwyW{V;oOO6$i#N#)Fm;#tQOf0&DG4~UNrc@~C$en~wKG+8 zUy=vRD-F9gaLIqWnMmke*iKuYy%3L<;Hu*YQL(Us2UEszSbS)Snk)yyuItOS*oN=~ zbfi3txPJVtQuim=Mf$Bpqj8Q2T?kD@!*7H)*!Hbj`9y9#KE7j7)U(%hA^f~udLGd2 z7gQAYu!(~55g1*Tw$0wLd@nQDhKh>Zb=VzHytz4q+jM_09EoLj(bD|V)s|jA?!{{~ zJT}Afp`kCrF!)X98ITc*>@4MdX~qVQQ>bL&={@T#{aOQouopHX*PqZVwg(;}mbVNo zSM_YX*v*P_u>`hIdeG)c5%X67=f&5Fe8;qs{OJT*ZPPX+Jz4L4((9g!5PGske4;J9 zoee48zR`chiY&_&!07&*3ED}*hk)<=EYxt&@k*Nav}3QoXMXs=vzNzN=q}1KKT;9h zP_tJ~g@3mn7b+bkd>xn01zMxiJHr0)GaBS$##7hvO#_6<HbT@M{S6CP)>)vdN<E<; zvMW1J1`vx3oqF@mn^%w^4FSE8;CW`{+CJZL&bNQ}45B>-nd{Zg5LoqY$x~=t8Z?-D zm0L7ySe-M$kU2<wIJ7FQ^6VmR!)spn>P&R0BI)u?<2?Ue(cLZ%G}U<{L~zt`NYt)m z72vBN7T}ShU&rC&xT4i0_{f+|`bvI=Q0|X6b1?y!r!L{Wd^?Vb=dB2o_LxHp;>F0i zVBmkTW-YT(KjKxoqmolRRHRChezdNW8~aF6yQit{#9?rPQwukgKFR}zm&-9H*c8o= zQ|W-4l<rFO(|h|b3S21j_J<GswHj5f$*_R0t&Zb^=9XrW;4IigL~oem6-|<eaR6!; z7VsfJmT>f~KfsyGe^Nkd13_|mkpfWdlyZNu6HB3?HXT!EqU%K880}`yUPHjZswt+I z$&)2BNvckWTF1A;9KK4A1IK}i!nF^EmOH-;FGsC<Nq~9hM}fS{!CUco<dg^Z>z2m? zv@4M}@_d^*KfvB}juCWNIHbfVloK3AUZMVi0;An^A_Q}Y(RM6Llrn>%50_v>tgU}i z65;&Sp-s_~CW$4QSX6g=?LR^epV*7|Hc%ZCcau0gHFG^Z=BpW9<hVKeeiCUsw<$v0 zW)m7s8Bg1#4^e}ZQ89eDLwbO|a|T1l>ebcDaiZqa2CG~go1j`_nKv=&cNr2fw!+U4 za!{)2(1?~vXkPl5w974Xn0X@ApQnG|RW{Y1kG+y#!iXY$91(d;d(-lxVLHBInLyFC znOeZT=C7h}$m!yK-Aoa^4!#2$(<`=pP^z>~@S>kF;)pn9D9PVw^DNA7>K!`1K}k9L zNkB~1#(dfa<KTTp$!259psf<l(~ATtE6w|cix0=Td^7vXl4C_9DMx5ORtJCVGP-aE z*NC)>7IJew(10vSlSYHzaKH`}a7zd|T^P*<_vg!(-q18VmVtH2CI4ZmRyL$Uffq6| zP{}F)3hMIomJTFC<VvyikZzLX3_Z-d*j-$JVa-2&^F~&G2ggQ=iiU1{?0231H4djc zGl-CQ+~)oM!}+Id^-SVhVfTMB(Jv}Bzl{kVPgMcpN8`~$WCdZ~4_~W;6!{DLtEe9> z<pTHL3|GRELdn7<5wa)IAAN4C%>BnujX^QINGq9M1h0?*=W4`1RN7_nsARPfS8{$# zxUVD12jPrfmJt0T6Gm6^@v&4_vbq|Ek4K+~;+sY`pvdY!SB2$p*xP^Rs<FX$6hkPp zA9|l~vbL5ZHCv#5uj(DkOx{ItRU)7q+C!%gM}4KdeN&9?6LgRu7w4(8J)%GG3A7u7 z-1DX&{Vm!rAF2RJJpg2U);{6&%5l-;tNN&QT5ulJ%6S+WOE%|@;@QZN<((O9W+PvL zD6V{E07F2$zwSyh<R~XAH=(Y7V%jFF?L>a`bDR=r_l+9$zn^#49BEU~BW<QUSGfxH z-Ig-4W5ABN^&e-8vly5BhZDJ@ZeoS7&IgNK1R(>VIF!8vse<f%t)G%!7>3=v_ZL#U zMd3*(DG+FKfSAi&)qK<ZkpsWrUI&$q;Qe-so#G^HZN;$S_1*LfHdp3<N0VMh%KiQ0 zYCbrsA(u+kRur^-N$NIBUHwXr8C1v_-^L3cXYB}}IVdLU_EY<_QT}1mXeSHmV&jfo zQ6-I}uDp0{eT$a~@q!gSPgFB9nnOB8I8K9`I`L&<=rbcYhEMaS^EdnUnp%T}G`qsl z$EA|FD3Q`kBbPb9>^0bb)^vTB^!c-k0|q(Z&3}{Hc%Vc@TM$QFUSF0vxb|Mic}kGn zRDbwA@-p$u^PPlh{?p4kDY~+BE+si>qL7Gmk!uMoEVTyOLI?PKpZZgIrS-x~%h^^9 zZBEl+cz9M0wLF;cZ`cp)VDF_w-ovt!lbQs}rMx(uCXAAQ{4QjF4!55B<&^(+c7pqM zb)WB+d%uSA%>^KzAQX`V1{-+_0dS=;PqbG-FYim#w5A1AjJg^Yzd4jtZm`TM4N$$J zL@5Y=6CG=2)XMX&L~?gw*LdRRn<lLQ2bUR|%gN7FQ+c_ene=!dBWbC0f$&6lS9?eA z+=8VJ7A31MP77dv<agSWdXuh<!a3t{_DDCY>bn$!N@6DO^b2Kxk4r3+ZyURbe1mWi zdhd`{h`!~3kqJ!8M_2E8H2qCp_2d1dhX?0ODto!*)J3U9J+|yWT`Fgf3|aKZ1EYZ` z^A9gf*1>+do2y%n$fx*wEcmyE3IqPxCmn(w`k!7f*LrP#!&2*{Z~DzZ`1<^W0HXzr zgJA{y(+g}2uWhiG-xyiEy}tY)$Nk?W>hHfBQTe2DM?*kMA2g&wqW9N59{+io@*uC0 z*&0n#mxj_2fTzQv=Rj~o26W(TUN)>nfs0A~+y0(KgO;_co~vC7iy#i1yZkle{`_O; zpmL;Fop%I(FP#?s?JexD6TtU6JiL*r4=9pjgbt!j6xn$oA&k>;JL{52#eXsk7_9x0 zu`y|Cd)96Ib+p=o04=0|c*DmDz|zuE2E;k-e`bagoQ3S&^+e*f(-bD={VSMbBLjDE za2PA%DEpU`%0~D)b-I2Htg84*Ci45APT_Fm!;oZu5>W)r<YaKAD97a(cX99ZWtPW1 z;@UsPj5zdjG!e|EUw*;ox@`Jvw&PPQ)jA&XsOw}=(NWU=``=T+hyupwX-$9YXWn}d zZ8Y}$F+Ls*7bY_F5O-<uGj8VFCX(6!NBuK@q|nC4i;O7^8xU=j7C%J3Zn)8u|Nl?l z1gZdkOThU>0=^;hyN(XAuVAsPaQ|Rk63C+BW)d9L4_}qMbgOz(Q2!}>LXl+k=uZ4# z65>gwfV;%5)KBvILGuqU3(b&pBAum4@Kz_hZ80J6ePO@h%1b|2RY{hFEwL$@!5dSB zin0T0WK31AyJGb3T{}Bw%s3`?Y)69MFWCow{OY%hnJh}YBCO1gQHMTN46DK47V|*b z#)nX1AOxUONbFCi(b^ZCQxEHWUfuXsoxPJ#H%F7asoKrBGW}_@W{nU*nez8MO&Yub zE5~;Zhcy`a-R|0|>?eh3YUdv50E_q|Nr#*^SL1o?iK6SM<=c>v250NK0@u|+pk!%( z#o@>_be{{3wn3Avq6N0g#Lr96b_IVLp1ZaO(-=;kW9diNW(!a4p;^02q5!;)&ncD} zt@HbpWR0oD3r}+l>Yic?+c7OVg@h*#sq_9{Z8M6<www0yE%(TK20u<R-kLvJ#hhVb zRSXa{t4*ZrYTY>KbI960{wETt6N8<9-nm@Y-+3&!uXMi{{RV^Z@)E-|xf+;+Ig@;0 z{@CFCgqB3a)+76wKp?D#+2nKEj*>s7lVJB=w}Sgkvo>0hBf~^z<gHo+-pJPDHu<p) zZCP$9{@Knrg}a*xQJa@m&ayi+i)g>GNK9<1Q~|gbs}tJA2H8tkti@^QLnc9g0M75V z{zvu{4ne}Wp3{K<`8&Fw6s_lHC=1!EBQSb%5+E^@-UQwjaPbuun;VG~ueLz>cedVM z;K~q?QJW;s597)h&II2CSJ#$?heM<EeZHwesXA6jHddo-HKZi56XZV~HkeR_UB>mY z$hM~CuU#K_p9SC$otng`>o#G3&g|O+3olDDofzBtGUzoisXD%^@lB0sUWqmaZGWHA zPAm=Maq~Xk84sG~L=isknPf#%l*fag`>1=(i%*ps$lfT^b&0YLxo-m=!R)rTKgcnJ zL}|0?vh#$KJPUu_$J=HC_S||JTBj&$k@@k!zQ@MB&W;z`M)N_>N-JuA>HsKN;^{u} z!BCaN7!!5XhMaPJbqMa;$W4O7kuGkdf)v?N<lK#GwWKre^Q(t=wiUSAOZSubZy6ms zuHBSiGK{;%ur06@Rc|ZabtHmrr>&8ak_}2Lpp9&OaL`-0qlH`^j%<W~*bcbjdoFA% z29CJa^;u{E#6gbp`-5eFlOtSAGQCOFDO?s@%Q=ip?0T*7-mFR9{!l+mkmu${YVRA9 z%<AVBb{EMO_@Xt{M;5n*qC@_ZlyLr}gWzAPSezU-uo(onQK-M!wFT0{h&i!yUR)%_ zfV&}ZDwHJ9P$?|lG(L<mlBJ26+sW|0DOP(_BBgj=UzxSfG)d!s4lCAt6!vN{6(t`l z0ZW%2^@|HpLFF2hN(?^xZg^5`2LtpvI~ZY4z|L%_$Bba_M6+sW*k~8;<!vt9k&Y!7 zl$h~V`YoA&OyE(t{`q0q*rMHGxQZ-imUv?|=xJ<o@L|)~>+ysr@7Ds{x>pk-#kU?H zv|zIhm|BKh_u1}$NM7{wjhswltNF;l)OGNb6oOqPjU{V`7yi06L+7~?y9`8S102qa zftxcTy114Lw4S$9L;?e5HB6GhsX{I_p4g9>-t;4Zly5;fphJ(dFOl#tYNM%Pm9BaL zlDx#OQ)l7l)zq-)0|Jcjl7(sA3VS;ZyIhkwR0RS~g!Vgsl5O`(G1LkzU<ij{3l*33 zDU0n8%Y%FkJeO9FPp$mS<b<wMNx*~kK)Yax7{Po%=vFDgEgwHF9HX7zhvXCPB71a& zh#a31<=K7NR<CAg6KS=e4oiN{ZN<`=ie;zB0)l$ZuPQuE7{;+}Y!2+M&DIZ_ajsv- zyb&#rLFlo6&Io!6HCyp4UsEHe+FQA3JKm{pPi&Y$w-Btfa;*w74rF$af`0XV9Q<V< ze@Ftmu*QmPY`i?;R10~g_=1S<II$6|QgL?H!}uj-N82}GN1Gv$<5WA;_Zc;iQNg(S zsCs`Yr9ESytkmS#Lis5u=-=?9DgMt1pTt!jb&Vx|ksSJPCe<<{mUVcG1^C_OJM*cI zLh6o$BR_XvbXQ_((TkK2;I!z|23=)}4fOMF^MjIUS!XcAQ5E-7)tY{U<<mB8h5(nc zidQj7u(F2<A%>Q$$967F2jXtcquq<CQr8_B%_{!+N#we`2V@VAN6+VSr_qAnghv4k zuMInY8d4XC!p<ifojLGVoKdA>I}m=)$>NO<GAe#KQRIsr;&e`^$L=0EWSx<n<RL{Z zP4W(*(7svFY&oo85ETRFC!1jR1^rn3+6Kkb7rYc-_b9W(6I?I!gPwOxBMr`J<mQ0n zI~lPOYTM&zy3{#qtF_g>SjF+jWbZ#nq;L3t92&ugybmVF-mODGNYDL0cL5yvxLI-{ zr>&5*nUi(sG(oo_Ow1PB|N8Yl@b<(Ic!nBcu!eTb`%}-3&%27c<4QvF!UcoQ4Wo;S z5`ND1bcE*z2Wd=yv>?^(NeDMNxcHHpVONa}by0iZl)PsWhAJt{vY&WDX$-tZ@X z1qa<2LB^HZ)p)t@kDhS-Bu%)jI`!@7J*EQC>|Y-p{1ThL@R#J+`Q?Jk>bfrJM+q|y zxQ6U(SUv(>W}>5kspHi|gzSCEv<~?<^j7H0)9g&m0No*Gb{F=oR+A7^-a0_-TxD8y z;V%|hJ^JEgpL!GGWSgagfebu9zW>pGIQ=Ei?A?7AikxxoXg_?aaRTE=c8<*t>FN|v zbx5O#AWC1p-%L4q$cD~$uhvC3k#dJu&OzSzqC|mg^N2OVdE0qLd`jjir)mzKU+GH? zJ~wip1^kShO1BjKVofKW>1ooOso{>v+b_x?kp2V_JzPWX6k%THz|7$|`J#b;ZaFL{ z4%*3eu9I!mP0~%tR=3h9bE{Twy@5S1S#lfO`3!_7VBn9)*~SgUpCrW8Rx{92z$}P9 z3PjJ(8A@=q%w|t4Zzou(0Uz1-4YnDWbCf1VM7qV!Pd!fi5`~R}1-bTI@9s9GXMrhA zb1yN%s*Y3(Mypoq5bZtr({R&&u3YD#2o;1eH#>{wdsjW<cN(ZcE-|RQ`QD4yjw7<> zm|}?hVSmUb=N1@}{0rghL+C0NVVl<0DDZwe2ogkcS!j2@%P70YBKl=Bf2E;}Mqh=W zXC1h|Qw@I*K2uH1nH2@Z9?kH;Q54|(oP?}BK-?Tx_fV|n>3WJAmM<%R*p)o6trTCa zc;BRz^i`ri^CHz@qz|OBlf$1z>$LBv-^L;pMRAC>!RI=+b2@vYzK!FYu^p3c;L>2N zYtUacu+7d?pPXC>l!Rh!!C4{PZYJ<8!)>vWJldtatvwmBbI2L1KVZsH3@&_Fi9%FK zZ^imu5be%7q!?EoXSf=F$$uTKDP!7F(w{-OV4`90zNrK*Wz6)mn%Zy@fduIOzI#G3 z%G;UT-UDv2P%F6+XSBT-m0DW=8!nx!!%;6xJ%9bM$(CL@2kO*MxFtzMW{ReYD1ow% z{n53jg6vfgdg*s`H|lkLikZ)WM$$Q#gX@DtlD5ziOKxV%A^2#2RGw}Y5AwLk1{o@C zTzr)C3d1&!=9C?J+M#C9n(~f1*oAQSNd_Iqa4zJwuzHh+YT)TLWwojEs@O!qmMCrt zH>={^2f!u@VMGBHXZMp6dIElT*caWDD9cI`M8N!ly!O5E&QpwG9=Vs}2w&S}--HyQ zmj5lb=GNNHLHj{}5;N?CqSA<F%wSC{6sV)Blpd;%W_mTTDC}<GG~j}Ajoi~|DZw+u zQL)op##xspU3u8Gf5&0LszqjfySB<=orPef85WR~A)G>A+NzfXg)i<=+I&bxjS3|@ z0NtBL?|JM(^c6)9iyv8`ysMPBT;%x^F{jP%7I*o+Mwab=80j0)nZBrAZ-}}0HFpg8 z7mSDKUBeYVO}&!-Oj2*%1~WemW`QFB3_fzoq;Vc67Cm3IlsDs^_Xd~k9HB3Ci}}Q2 z0r;{FH`6IjUKEAGNS7tNa_dRd1E-Fz@h9VcV!Hme`lC<t1(_}{eeh+v=k)_X$!qmY z9%D}`cZSk`uzkzNp%VutBg@nZO@b30kU$c_QD9+4-LV->yka_V`SQu;ed&>0UAy5o zli#(+HfG8H<1neC&h?S}V>$TM@5$}wP!|Edg2+UCMKm4OiO7hEVj?2oA3l81m*RQf zB=^dDTjVy<j2b9@k1!kG!6EwY2tP=ag8|_t%U8pH+Y4+Da(MloYSM0_Y934b=gf`e zF6tQr1YrHAk0LL`dF!1k>wV4SzV?@(mWGi_vI9TWNOE}UN0}>mt=X*9=_Rqv_kQOr z+Lb8coXm;#Gtpk!o3D>JkFst_n^D5hZ(MSRBIB-sADJiWy{>^-@W0|qA**z$EsPb6 z=ly+uF8?*!ytDV4c#_|b$CWIX$EX^Z6ilx>MlV2xtM>hnV+%YabVPk}lViRvdYu5A zsMXd`{2*%ppwChfM5n9clzUc#GE5}J(&C~~`+|5X#Z0#Cho%H!c|$twr}+ZuEaUfl zOnIEN>sZ`Dv8ZH^by8^&g@(c(jyBJ{`<u6a`aDG6;D8gd<tKf@Nc?bxw1Y;wWA1=^ zaJ=$T`<fApij8jc0<8dA3$bNm{<imTjg|t3)kPqc00p=iAUS{GAh0uBZfDrm&%67* z2|+aty)3icE@kowF3ucgPtH4bFb<a{J$l(AV&ARrD{~eT?wsN21ykV328fOj-Kr~p z`l`mUrDhx4k%Cxdl2u;X%^C`BjmkHbZjWKuvRf_2t%AY4k=hmgnR^`1XF3=9^rJBt zeGB#vm-WvNE5gA_y^1!Sd*RG-2UMoU!`ws66$2UW-rmMZR!fD?HJag_fPZm#7TDv} zW^^bhD3^FfI=Y^llNBXZ)gXm*%Nyi>3iMbaa(lE_{*Jr*A*W^edr~EHMV%LeWnakI zhG&=Ijco7P&XMc(3@h_(xf`31;RO_XVdZ_XiHDjQUlW!cePSg?xVlR{(VAVeY|?47 z12WLHLI<T@SNvvJ5qRi``Afee;d8E^iayN4ReK_JH94ZwCjz3DDQ=T?8=8WDwBrir zvx_SiDRnwLl;=S8!phn3H0BG^)sJ5xIY!h8^}SWv<ZSHs(RI^Hw%h}#VsL}XFH}d> z3rk4^4tT#d)yC6iwMI0s83D_d7D@G6l=UymBv5TT8UZRq{ooer)!>)!pNHX@sZXIb z@ib>n75vi<!deBmT&=O?w9mnR^uB-n7xCxVEvwJC@!9_#`7q529O6AQ2-C5e%7)2^ zb~MF^bws$gdw{G|RKNWJS8Plq5*m})yRy%s_p8MYnC~S$y8IN!wSnw>Wk+T6<Mg?s zE~)bOBb(17Fisv2a{LHsC)XIMx@kdXY2!tVn0#W9P&qrU!=SA@l7hW|424eY*fFpg zB?$Hv`((-4Z-M4F1#SR=V>xc?YGb>6`pW7+)+^}kuK%(xK>$1o4q1&HtLP^NfZu63 z@*B670tbMYLh`&kY_=*q7!j?zKcEj{V;akiLw057u2<KQStCnM8;EZ@E04-;>9=%Z z_yvm0Mjgo>*cz(~X<V>>lM*2-T)|H<^rLmAXJ)czexni+o*axBzi%RkbZ$CorYyx` z_66Qk`1i`;2?@>5;)-Q&X=mn@b`M?fwrg0-Vb_5*^FXl_1&WJrYul0j%wdl}S9R0Z zH@Q;-sHANlLOiZrx=kffO6zMnH&2KPfY~ka6+8=k@<mJ2fK_#W>ni}`*IG>a(bg+7 z?yb>>H=U$FdKj^P+Tl2rB#&s@6Yhz8%@&rQBYk$3ojGr#I7bPMtIb!R)(RXj@#H8| zlc$}9A`BQW-BUlp`P>M|z!tE|Yk8^%(^-oWjG~c357SOoH-<lLVK~CCt+=T!Wa-6C zhLqaqMx$nA><>nN!mopzBhrVaAmHW7Kfk!S>HqQ3>`RO`KQsza*%J@sCMleb^B>X5 zxp(wUv6jj|G7@oTZ|?&iA3<?(@!;_Ax8(;}?tIYUTq$;UcXyS=4CPM&Z#)YN3$4XH z>45qcWw^od1?PWI+D;*7lL-@5@K@+e)c42J>yb`v!Yf~Y*Q!;LenX`|QgW%h&4JX$ z^m}_PO(%<5Q`@&i=O2E`jx>ez9kUnxDJeMeVDr_+l8}%Z9iUp{0b~MRS2={j8~6e; ze_46IB>m)6uFE&XNq<ujAx*EPpEhuLJZ_)82zi_bvBYXoYctRNAQ{oXXaS69=B9r; z3ifB)UX;*(m%QypA23O%lp2uZXRl%;;7Xmv%qnV?fV|(kWPT2R2_v*Ow9^e4@{O~z znA5dZ<An;{o2?}b158XzSHbIi$@-;sn;&^k;3!fX?@_aM7byHj>E9AXo8O0qhfiY| zL|>PdgAP|3nM6cI2?NekAQ^H0h}5jJqoHpa5iI|Ior=CQpZhsUctnK5-YD*35_f-p zzhh<P-qNvrf!Zs>|50Eh{8p!jDY9-Zw=(q9Z69>TD7I?`N}I1W-Tpy6-20<G?;(vI zzWw7aopW-2!<GO2KodAzikt)vVgYo*Xs9=!qVL{&01q`un2`faMTsNnr4Cu}jILu* zlg0ag|BB#L;H#HMKkZqV|NYUv5BB!^GW7Q}=oE7e1XrOl_<;}5l${q<tec(Vh@V|# zgp&=JI^Ex0s-HoR7RJW2L6bI^V{bNcxpCIWr%%#l&xuR5dyreuuSPMfAa{9{vENha zj?SACwP_&yZ-<eUeC@pKKq~jY2iHUz`_yB9meN`ZX$a(Jrsakwy|*NgS&t-cEbr7d z;gQ3|L`gX7YD+wtVq`~*d+Of6M-5Q27;wn5$teJ_CMM@B?gVgjb!I>2GLygpl}MQr zGrPOC3pZytz~jK!WB5gfln8iC0tJd7c!KKdlFORTQ5Ep~H$E*@u`fzK%AP-ct6yAy zFMxE2vzR=3LZcp9-0Go&O(!R2Fj0XDmz9S_L=gFr=Tt{<kT8kXvE^H}0hxz-fN@&5 zYeG#EBzzWtN(_OFk<iImN5(^&pYi=;cGC{ql|c46J5g7c%A7gjk`ibZvL>MsuZu8e zs$cz1_Gb<}NCwvnsnC8y3Tj!FK-F}A3deF`V>>4HU2&Sm5@xcR(Ahe?f5+DpL}j01 zPpK+^8nHH^)yHqy7%(vyJ>)Lxk?e(y32Or*S};*T<a;wR7mn0b6l^d5rxMXgK_+Bz z6`yS?@$cnU0s9%dqMEuR-cCqyhLK*cP6fgA#x5oSB=qG=dA}_S%)@4nOX^F1^-08e zzpKH)Q$UrgHZ&pw?UWz|xW~wz(NtHhf@U2eQr8caK3BkLE^&7Xq~I6&2Sfx`nd4{S zFwyg?q8p9#E`l!!hsVz>J*T4QEfe-aUd~jmTPJR=IX$uK29wp5`J*{YH9WQYA2L$e zFn&Gwbgqn`EVb3^*S^wFI)yHOES{nxDL(!-1}tALm<O@G9g1PvwCc2SuN%nWkHFKA zQl)YXnYo5f*-a-v&nbp1$OwVdfb>Gk;T4xf_s9tDNH?>};2|p5ug1lBCo!HGKB#b7 zIlCIn4^9LU)Rc0~*k<bEU8{&>qdsfQF4IvUhjGb8ox`Zjm=`P`<+GN5NS<7=LoQ>B zr1sFgEHODR=b{}Wmz+rm<gKU1q^%+X3glK|x57!I@!$}^|BhwJ8Sic%Tdqcbkp25G z4}^b>h*Dh2O0MsU&SQ$^w_ei5%}p@EtWvHh1^#%7FYJ-`IEY6?1$~wa-tSHcd6D@8 z#<j}Ici5x9e$QRdE^8cr%?@kw*5IVZj8ygtDn#n68u!;)_2#|EPOy<F&Q0JzQRu*K z7a5}-(+W8$yr@=G@q#iuSk!iK(sH|sf$Oh_Xbt}+6TV0Z<re-z7_YWk4>8GU!%Xz` z3&DN84VbP8mfC(soQOtwhFG)n<zN~rL(pW{R%wkW%Bn!tceUVucnHr=l#;2@QW*N> z-(f<bw3}RQ_EFM$jmEm9``C;tED2jsnsL3!=9Xy6KtK}Ym#g#wkaBpd88Pm+c}EJ- zt>oV>S_J9T9iv9_^JFJ@z0^Xf2%=~1cHBHc-a&<ZTch!56<e^5BPd&bv+Kv$5uh1c z99-m0S$QC4;3kTHKw$M+KsMc2_r)4}{(R<8|EeZdpZc-<b}^_Xe*b@0mOFRJg#LHM zGqAI3^+U>2fC2p{nJSIdH8Svm92G}`u<?@wcv%~tb>*Z5O$-fTrJSE(7IsST%_J$* z;l&9FK2o2QOnz^>KzyINZK=444bGWau2x<i^)VyznWHa%jFb{wg-&N%Q>F3IsDf;b zs%c|=XTC-wqEc}O=^~~q$0j27;(RWIe1TlnBT4KNH8#L7Z)&nz(>rmf)qgDx$iuig z%fkd8TF?tf&|4r8(REuMS%+CbmX?_C&!JXXh^$!x+?Vf;G8nUzNrDF>omA+uNHw90 z%LG9yt(NzH;uBZ&N^{Sr^G7~mrgDxl<gnajv*7SE59N^8xs%b*$nXw7Ut`NJVPi4n zPp5Y#MYYzkeKT(9e8{tsz{M4>TAWJUdRQVh^ru%<Wwg`JxG8=9jBza9kdSS1g}S=1 z(n3D%llcGcG#*Rr)ZEs@c+v{oK8ndLsXM$-uPp(8$HY7xd1V4nx9St^Ak*q;!D>%V zWO~iO87L?unXD3#Vd~xU1BK)6`!$*AR`&|6hB~hFRy52Iw=Nw9Cg+I}RgWESJ!!p8 zsT;R0Y-V}N|NJl+66MAb*_mM^C8T&gm{{=@Fdmb73`0Dg5&g_JzF4keQY+yTM|xaw ziRq7j%u+cyRZEWzO@_8+aVxPBAs^vXiWKp>DtcnJ#^X>!&i)nj=+HC2!34=z;EW9Y zx+`iBZYj_~1E23#Pkimr7hPBL!QN!L+rd*!VH_@3E?N%8{ynns5e7pfs}i+mfrD8! zi#abe+LkKO`W^i5GJ3wt9rViMhdd0i?#Y^e3ChtZXGwAV%;d$1K`}$baL=kB>3kA3 z^XSclWJdwGFjxtWaMs(BkBBE}!I2Olam{r4t-rX%%O+)f=c27X9^%Y^JE@n-3P=&! zBQ0xb_X+YzSZvmDH3*tr?2N>Sr%$aLRheDxG5xoE_mMpI$%5koYo|-cq+y|A%*7^u z?m}?LeDlQG>HG84OHUep6wb>bfaO!KU>a|WDbF*P8PmPuSBRKAi-xlX+5H>sYV5PN z3nptpi|@Ek_S-wj_P&;$4C&u__qOal`ikVUt5){%p7&q&WLa+~YF{rKgD8)2+wR;^ zPTHIYTHQBK;M*R5IVB}0rdD2NKC%mc8m?tNhkjJkl-ETHix`OY1q5~4M+5<kBZs}| z&KrHl7WDWmpF5;euBM%&jtt-u0Zhb7^RLVyM9pA4{K(#$(`4V*FkSP;m%nBYDFIvK zi(q2Q_z1q?+L@4f%!;mQ(&dwLTf+u3rTh!kblHPm!YK%cX;jA!?kp`lc|GNSI4N;N zjLE;yiKz_|m-RoO!k!#8GoA!XmU~fa8k20AXLA&&VooKCX?I!y8N`9(zF-sT5E@%+ ziiSn69TEUtDr7tp&K+3nR8$Iwfr?wsU_6`79kor#e>FEy&D5yjINVP~%pl>j@<qo^ zmr~PbmJ%zBM?QhuDY>k>@{U7)kjXunIC1RCil*c6%jr;0=-Hlk43vIFJ**(uGb6(? zM&=9SBU&cCIy{|Hu5^<E+)16WO7XIL2Zh`KvY~Gn3b+!Pvn8123ou+Fp8k6FCpV<S z<^}1yhvws2tX(L#NUe~1Ny#yu3OtJZxpN>(80ISJfvA-L&LpcXjt?h)r9wQXQ6H78 zeVgkuAJu;WqZ(w5;81@T6EY7?$7d)-Yp!Tr-*u$g;s)!SUyy2B58}AU1hnT*3!e1* z3U+5DpOlT*t=wRv!rw1EfADDgxU0W7Z%M9~&Vo>DI@V20bnIV$_K4<GI~OquPoHu} z#`Ehs-qoi6r9IMYK-XA*-K&@$S`dg@(%2Dox9r*V<ZiRaapijt*Esz1TfG%<=Ebv1 z8kj##GcMp~f=(=YH$`?>*PmF?bD0COWIvW3jlCF4WhX?*C+utW^@2*xGrPyM>i{Cb zpRin=-`1M{kdcwm(JEC9h0-j)ElpY^DqA0&Q{`$qeEEr?PaA)KA=vCqq;R5Rz~p~o zFqZb6tuI*xofO3`e5>>U!Z*2Pw3B~|_x!ez!4tH+r3(jmWo2;Xovi;#7piidbbAtr z5myq=4XPTNi{MH%9^P3%5}z@?-~<20?R)A7BMOrLkGi*vsxx`^g%jLegF6H#3GNyo zxZB>iySoJUB)A8EcQ)?s4#76=8+UhZ=G=2;=6}!3dOyD(d-b!bt4q48`}tLMI~}F? zdCrGSVM$3^`2}Zr&)osgae2nhp}jPTrAVO4`I13KpKCGBk_M>T?eJz@|2u+M4ml%_ z3rYvmE85715&avB*fe+-EUH5HtG%DfpD6j9$ZpcZMHLo*c4e2mtnMd@tgSqd3zK@U z0f{CtpJ+8w3kI89NQU!R(ghL%qo7QF;Ra!D**X?KDQ`re9Z3L+5vZ%_z->KLn9D}g zl5`Dy3sd}MnlDto%L$CY;#v_U_GKOcWkaTj8a$h&$m>KvyRgN=w{{6*v&j5(K1qZg zy<2qgAo@0c$NKUf!(zVz?_y;hua&+7X7U%jsH1I0nK6FvhjPu+zmT;bWuXc<JTN(5 ze=arSK09Ld?I+4%TL*kmTRpQ{c~{8k%W8>T%BcBuOw9%}pDR};o^{y7_r>f>`W+6y z+acnqJ?i>m!y4im2AS_=V6}XS6%x{z(fTwV-;S|=G<Kf+Fm-Hhgo{K4@`9PrJ&aZZ z+I{m+r14p;elN^(s_(%-V8%1YKa=&7=xnOdye#<!t=;`nj##0UEY{i0F1+4m?n`=| z*8UQ96>&Ungv&D+$a7!dCxEU-a@r~Dc2)U)<;me#%&*&J)kCwE7a#caOcJkt+Mh)y z-wPCf!iCKid{gMw9!&>%>ca>2s$c^<(K&q<hg|M}=UErs$eTi#HJmu+9>;eRPVN_e zulU$msb3jb#}e$4z%IYHw4962o-4@eS#f;3k(<iuM%<0#fpi<Mrq8g?Pop<#$OWTW z-|S98r7ARbURE0|oE6ens=7=v(WLb*dbBBj?`S5n2>)-au6qr0wdVyk3i}L<gy^8@ zFZF+@CiH&O%}Y($6xhS}&WD=R*OUVVLb-Vt797vtrCd031WDlEF(^7dw>i0?qwTRR zJC2K5lPhjo<TXLaR3Lm4DAM-!>MKa*%d(0JZ)BE|&M5w#m6~yQs`dQc0NcDG#S1fk zvw;fjrX)}3sxlgL^^F-lAd_Rp_du7{Zzg_-w%~rNeh57$l75c-zR8O|O^0^VO#v-< zRlhn(jYBF{=R`b~qFN!j-g!#OD2KzXth9{{4IUHvw08vBil2Awf~eJ7cC3llaR9+U z@B9o~@00xdbWpU)SErTfeu1cuxc+~Crq%phz~ZixJ)S;j@|0ML#8~_=L!k&e-X9~n z76GcXPkXB{gf&0{1c|qFDUjPv>(Ez+M3D%>emFs#!x1li;Uv`AHrco?#5dTH6rg=x zW6FbtwrpWqid4WYA6BJdMas>dL+5VJYiBS&mn@(+w(b2^rFXtKz`C(e=i_jHyJF<v zqq_nL`rgnQMp2s8Ieg`Mm!-wUOAB$fre~4-B^YVaJ`Ech*F7dgWpfedN>&#fYNwj_ zyzIChD_G3TZsf)QaF;%K80CC2*~wi5*D_9PAaBw0Ki{Z2e;>B>_QBfmO$x|Hr`w7I zoA&J92pP?V#ju(9M$r{Acb*@AW`?XjqI~JAa`)(e4K<e%Mr%^XJ8|W9a&WJImmqc9 z-C?Bg*oU^F3~AIX8!cmG+b%1aq69B8O<XChVB~dN0DPO{YK{}O{s(KViv&fYV%ytu z0TL`se9|K*bW^0B%`?~7nQ^y}@&x+PLE=H_T>V-AjLZO~E4zm`X*uP8>Jd3J9AQcF zoQ)a}<-(;u#u?(<CHEoQej@U^AC#qAwwy((O(_4mE&3`f##s+~ucrKkxiGeSkNwwO z&n{6wt-E*TNn{JDY-gt0Cz*tZxR&$2qfgw*hTTJ7YZO8JPW2|6)|=*X=lRN4uDSEY z32D>C1*(TN_>uTbodd6bcR;4SXSM<X-)I$r`&LXx`I4l{zQOO1SiKxez52{|vgzfE zy1^Vp=sX#b)3u+?EECxyL?;+;Jbx4XfrvDfTO!K75}Q7S$6a?L_H>NTQ_h5wUG?r! z9DlY%G&Jnj*7EyyRo>YPle33qtw8OJN&fU6#_Js=in*`<gNpQjh604i$_wjyV`;Z& z5?Zi!C=ht|4Ukt!l`TQO-Bj}b{p#Wid2MK}MAG4?m{-V56S<C3_VfDpGod=1EWa06 z`Jm|dvFSD|96_n1^*%pu6rbVGPwGss1I{;9y3-3~yNA@p9`1*2*`=dS^Q<FSwrh#b z>Bq}jf)7Rxt2WJl`bTyqYoLypu6-cq3}VOmHMp{4py%#cVB>Db?zCC28-JMr?!{(F zOU31RI@?dE5ZTl21Ij(Dysx29FmP&uoP}m;6DV6+h+a}9|A9waPkN8kZ7nvG_Dg5b zK<2<qv7d<F@pBoMb|_eTT#Hfa6^)_J`ylfn1WeY?H>f~=>AA#w_T|1s={-^l<CxOD zOMV@~$p5{#v&Y3a69Hv&89gPZVQl##BeEkN`WqNYuAgKN{oY+++rFy(H_SbtK(do{ z8C^ur`h9+Lz8&EdmO-&4Ytf>-7Rz;a^1O;>-FxJ=({}0c(xYL}FcrLUG4S09;S@Fr zRE$yN16E^y<NS<j?cf8_QrV-QnkqM=))j-%w-P>ojhPD}W;n&lE$I7ZV_dk{7J2U& zquUdxM?nMF$C(R$KO2wiPmT57XSt&{cUg2Gk)!p(N${h_lMf*A*fC@6PDg6?yvAd% zwH3o}1YxQ8CXM3+;7;~{G~GU2`Jgc<XsS|`P5CZ=7hYRajiBD3p+`~=ZOzv4Gvix^ z@(IJoo!qd9NU`*%%gZ@1eKxUZGw)UGQ81ohq7P6{?-u!#md|?zv^+C_CYo;hK`<Ut zK5TR63qM|vw}=~EG1X4&t9#?lQ}p4V!0G02FrF{P@b#}2@bYH7thrVUz(9GQPq?ch z<5t*z*M_s(r)EkLx_l4X-Y=5j-sd`PsAZElT$Us79q~H9#vaL%DbpH92(=?!zMBf+ zd68u{GT?szs=wrrS$)EI&E#Aa@tDPnD-;TK1Ki^m$Ugy)b2rzx??D7)bUQZ%qTV-i zONk(BD~&+5qZA=-dP}79@x#N(>j1E^f;vQh^lV<1%V=|LwZ9(owVt(MhDGc3%5#%} z0KyoRf}GA(uMq)y3wk0s_PTXIsjw*$I&PAAItJS5Z#o{*fT+Jb96BZPKR4@}A8C0% zZHjyQjK#U<CtbFidWOo)^}lRbY`1<C6GS)zGE+DupWQvWA2$k`#3lg|c+t`##72OB zU0S)97m{nP(qRNwtxioVnG#zmuY?i1KtDw7(mU6l)52MR<N@aeP0Jsc$k`f!6&l77 z9y8;Y&1<Ka9)y3q^u~`7uJab(%ky5SG9O@2)ljFZl|op&vVu0bpC!;xqd;Q!(|9b4 zNJu8|@c?U+?!_mSm6v^Em3<g-9OjjOr!0kyf63eghi`ZFScfdmzf?Xkj*m$^W(r_> z^h6_sN8VycTd(B#s>u-U<q)eG&h8wS9I+^uRd-hE_%tDG&W)%#AmPI5C^>MG04{zp zbcB&2e3LtIsRkW2@ff^U)VmOD2x5^jf8W7^q)z*90B3s|jP*>QhA3kt%D>@%I`M<w zBmnVY8L|Hj;oOLYpm^^>QwufnFBGWlmHzlbsggtKpDB<BF#Z8x{{IIX{x3Os!{Yz% z#r!%H_GbkJDFcJ-L`!jTm>+R*toCSMV*dL#1FfQxQb05jSGP5%SmE?W^I=6`Ru-^L z;zo41&aZ-N2KU|igxkv0&mU@knWK_j+1T_f?@KNxwuz$r6Y&S%R6J2Tq7et%nbWtD z#4!m4rHj!9g_OXtZ>WyHCFU_;kkzt))xLx68K?+h67!u?x@$lS(FnBMC8oFfh0>Zt z`jx1q-nG56CQ|$-itL&%bg9C?PYqqgz{?{36gr=a<f{Q?a&jEs2M!#6Q<7f90YPoc znTIbYC1jNoGkD+2ODoUDxGEXxQHN~W1a@4S9z`G<UvScI#LO8dAXl2<aJtsI0&?7$ z2{BjCP28S2SQI4MVvOa@vFM~z{YcPIlf!GiPMkl*QAGdL13Th21hO%$AQY_pH6bA( z+wb*dH!-8f2{2?3a#3@ChI%8!+Vr^0(O#QPL`Rh&t2H7nm_c!2G2sbOS%2Ezabk!b zs6?}|`+mOuk+o(NS2i0RO=7lPPBXzqSk#yRX9V5E*TqNRlCdY#?z~~*W-HZ-SI1Z; zHn<4^f8_F;M<sE&PD3+7IGB22-cy})TvA=KU^G_Yr-V6Ct+La9Ij@b_eM<;;tR*(L z*9}wq!vSL%umj+Qmse0Q+{#nyTFJYsApsiJt3z;dEIm*Kc;un@VQsYl2~a|oP44vf z4Wj%uJ@UAzEe7?%Z<FpeZ&_EP)!Fp>p6se>)GeA(Q~kXjgP?I!lCQ_!M<smpgr7m0 z@mGoK3X_Q|eoH}rxGjo_u|3GAPC4XaZU3XGv^`$XVy?R|%AI&?>t+ku!Yxn?$@Ff$ zosLxGVJ>6d_aoLbcCG>2fqq#<hYzc|5F6ylzi$Dt(dToNn6wpHW+%QA9_2JKqt6T4 z{w4lIhMRz#$KBBw>o+CT<hItIr4=C_5?1^VS3O~az-V-TJQJ&6&NOWH>%Jw$SFfN* z4XJ`J|D!iWF>-Ehg4NYkZl|3u(YrjvgyM<!>n+}e1g1(pEAEftC<<3@7k0yRyUbJ> zQu(O;HK;!n)TVZ+xU!_a>q<IUKl;m4olmX&V%as3;Z#^@gR{EaWdG?0g)=gZngUcK z(&sqno7sSW;uGU+*K>fPlDfStQeCRoCvAUkZIBX~Ily1tl`nYenOX^%<~p;<COpWz zl>GF?%C{4>K`0<#Hok*sH0y0P_z)8d%W<5VSoPyBal?@Ko+r4*y-#~l0N5VmQIK|? zbS_|-uog5;Cu>|eMWtF+k)HnU2*zH`192FI7N)*`UOS8%_6IgA@L00q7?qXGMM=4y z`#Oy}@W8G)JpWrU0nx6Xf1glAr<@O|a8MFyJJuZdUHwgLIZFH6uCvR!d`-sqylkyx zX`ich!zL_!xv@M|M#m!pxdU|?($yH>_dK60BY`I2Y`-37@+eT`5uw-C=M2m9pynEf zf5fqWC0`+^iiRI+L(i*8w{i@D<WuJw*%G!?!$l6Tm>`ujWfz}5rzObVdt<)SwF<gb z98bj>`Oe0prhGCg4O{sz65MPtVj`QO20Ra#Ly<T2oNM2vt&jmU;eDq%2cN5}_;@Im z2y(oHGkh*qONgiQjr=88qL5f@?vftumEn7TSV1<ga;7RDSKC&ZAU`=Aa9nZSVOZ1j z$yO?l?FR=RXXq!r>6h!lA4VB0@TE>s1h}OS9kZYEgg!4x<Hvbht%Uvvif&OMn?69? zN2bf?pIQsFS`%P7C`K0(O#(C;bn1&Uu7-RWb+@8WKcVlndyMU2)blTcwOglB!{q&c zYaRY7tgeo?>UqdaO+%Bd0RjvT4szc*h94Ow;ybHAU^8wUN4|x!u09f5Y1raHz{0Yg zC)@gggIy3x5}x5anR+Ft?{c(1oUVsgbav3{drQPC&u{7m={TFlq!X3it_-zsdT4Iv zPE&d<Y*$N;A=W<SEtf?7IJGE_OV!bT+;+ovjLWAi`Njefdo)=J82qKCtZsLx+eh2c zl$d67p;zAIpV|<{WaD<Q>_D2!Jt7oc+SbP8^qrTZiW#%GLS-q^x?FomkhLaQr9&^X z_cU7Y1nA$KVO7+WKAJ7g@~JTedH%t_)Um|1oEaYf6c9S*<?S#X=8K^A+%-#oJeeGD zopYybJKG%ixhX%+wd}d-hC(OHfhXyc=0gBr`*hKXU-Ple=cU-iMpmHIxT1)DE+taB z-{r7a%H7Z{s_g~tDn>10hl45W8ciJ}J8%}t;0OLl5X+BQgqIuq`m`%F-{!?dE0-Ds z8A-}D8A;5{nFm?ufqh*Qh?9?h73i#jWnDJ|Zxg8oJS`>J`sLhV+Ni{HDWHdHxssqn z0Q4-lA21!{!Q~F)5UJ=1*FyBJI-kudYQGNtF98S*%*r8kWNo1)0~3q64|LI8uQB`9 zLK^Tg$l4h-??BH^*?MYN#C{(D1WA;EBRZPL)9@fc&cSzu1V9BpvL(oW*Z}afVu7`* z_EqP}qKo~?-0NKnwa}6nrQ7~vavZ>V0yHreyM8mnCg;HBwIgb*b!HBJjPitP7=X<6 zoWLadp0RV{%+4^VF|D7d;@RiSYja;RA7SLjR;eb!Y3qGCT{@p%hasp=G+5+nIZ9ME z)yll-oR0fWt&k>gGHfA#+xwu?>V;_P-7t_TiOdKOdHubG)Wz}8V@?Qq5-nQT7<DGH z3UxL#C7>1W>w|}+fu0%<$^q9UJVnTvS=>n1L<YRnyUz5)rfs2dfpi-isu|O`!sm~m zvzoh7Uyn7aSn~WjdhL~)z|6#t{PCBAB~8gy*WUzKbx34-w{_Zo<SYXck{>1#&LW6+ zM8i?6wK5PvMS=vDi(gOuhA|)-sWjyeC;6>pXT!To$H&>TyXR|^64L=Zx475!dxA<h z^{Ur6vdjZ7C)4#@nSSQqBs3j#CWNnuc;*38cal`1<4D7n4>?$8f0fazAco>|Jre!{ z`+*qy!(~R0b@!-$cA%x*l#`r8_}10*h}!_uSZ}1Ofx>OSkP<$t(hMwUz?Mn6gzUBR zikyXa+fy0`X9?!?<t2%XZRqM{K&R3+9>6XY?7AxNRGH9?h1j)xOKN$@{bD>}fmUty z$ilU?|BJFl%cSruxqd#*OV?}2cMUr>cW*>1IvJZ@Ym(A`j0`1K(!;i$(`-_`N8GF_ z!DwYDw9aIC^B8Ef7-3Dl{P8XNHTpFZmsNFHX_G%$`!}B9?|`lIQH;9kO$@<gLHvbs zV^SkXtI3VCx5_NOAE7qoWukS^qv}>zS-;|Vl&j&`>zhqtXR|~`msiV`E?u{@m}QC6 zt&R{878AXH1}D_EyxfugGOohe_&(qcCbrEb!fiF*@?e+rKu2FKS?rLk$@=yFbgf59 zX#y_YoSkqs&pWp(h5-#7@K!v1M{Rwv!i%dWszGiX?<VH*g4~WfCGu(U?5Ua%A!B0F zB!mY%%?K4M=#Z7r^yn#hLskID*3lz3T&n9l$mKwPtmhkEbH(Aem;Tw<NC1yc+N+`< z5X_?2OwK&U3w9=4T$nB|>EqFK62yj`gdh1vGJ~tIB80R72(9#9HM@J524T>R;3T-} zt$Z!S45iCzm!lm_Y!CxZ=tXV6@^nBHkvyA*N(R|7Iu~8EIKK61F)5Y>x{_=Lr`3io zhaswe*tgL`<RM2Q4&MSL*C@VLG{YbUp5W3bQ_EYw;=KZ@pe}Ez4ezcqDn0vb^ODf3 zHSuv@z5;1h$H3G)y@>|ns;DF~mDBUE3Ic)%6oP`2$M@<g-g+MnFryCMTdgIvN5-mq z$B?ejQknO>Ggg;cU%)?!lZt+O@8yVDRC|AaUCF4)27ef-ZLlnWJ;~P$d&86!$s;r_ zqIz_>x3<{5LXI@Re#;;5@{5^f;`aR49PYfE9#q8Kk+?}(fcy1E`^A%B!02-!CL=m` z9h@G+o^Ha2@v4TT$R9F6C|SvAXJrU1+_dPvWJ=9i&pK{|Q3H~<UthLayq`8WS_RyH zh+ZzzKE~&>&foySQOW*FD~mb8I1;C}0+tXuFVay!$W!IX-E(fwuM%T))7A!<SFI@$ z*42aN^7PKgeY=!u*jtVfA~72O`@tn9Y?N7ECDRnhvetb<-RE<wKzLHWH_}~&RnBvM zXA8?&X9p@`wZ<#gfqX5Q*P7#;dd&uZUs{NMfpn<9%)y`EwuI<E?LGdYI`qj4Wz0{a zmra0Z*w8(xM{uyXoRKeU>II$RTXzeJr541OfrRer4x<dXB(l9usqcmY-EVY&>z5{k zsfL`ghm5`iPP;Hy=v-7n9XKSthKw2)U_x08s^@3G{%N0=xVvsE?nre-&vAHvwUjpW zQcrxHatnS%xRpwa<X=CA8b@K``!sBASx#Cn<%`+?+O?IMjp-UyA7Ld(6G7Vf4Dc~z zBVCD4czb-J#xf2pko7j?PkkCCYVGx{^m@Uu^}U-nL1>+^JRD*KVS5KY^PakNSWD7R zscHe{O4cTju<o)kMR$-J6>Fq_qi9={O&0&VI6FDfltB&<Ga#UErCDD!NP={HP=M;< zV(YW*3YKsFA)6XNp37AJr?ChmCSsHEddJJbD(6UuNRipgttrW9WjJ)DF7M|prU<|6 zHk*>S%aKbVeS5cr?Mrvya#`8Y$^<M{&xB!UEnoZvqgz_v=Z!1U?~L$&5xOa%<&<{) zC`Sy(*STjEk|uX@?)MeGXL0yhAGRf~S|RYgpUGF1xzYU+KI$06Cp~g-mHv><`Vtrv z462X*LK29GcxX|P4ah60PR@nfQsC&ABv}n{+b!#DLH=+y;4oC}mJGkJQcmnmMBCdM zqu^wC_T=QI$<%Q$Q;1@JRi7p5PcP?%$}%ujE?cGjuy$-oXeV&VF66_P2M|ICY`zwE z_eUfW^1+Tws^}|Uu?-wwJZ)KVraReNdE0j%JQ*6<W?WHHXkABj?X7j7mDV)i$(yKm zl4SA-adC5u&t;93?xcBZa-M&4|Blz09^i95d+CYxadT*+`itCugbF9zC<a9IU_A{I zlC=5g+FQw9oTE1(<{F+p5>^xswBU*n4)XLOw+ro$j)ERHrCL&wW=?u{a3yF;tjR80 zW>#Qua20#j)=JW>fMZ8`D~V(kDm<ir?-u#}yGF$1d_`CSjhjmb;tpPTqa4oL;X3Iy z*k|#SYNr<ZJY|r7NIZ(F6{l92XEkTOl^wdL1Yh|=4IPg>1GLFdnl>P%!>z;g94}ia zd{iK(ED|9}1Vl`DF9{<VG<0s@UAHcUgH%&gp@&hT>0zu9qxV?4jxD5#1LR<f46p|q z^k6Vb5#ARvn_!#)GT_2CW%a;R?93v*Cd_U8@ryxqK_|U`mZLfeG1Cv6d&|t~I?$)Z z>51aZeyBP8{LI7o>_(5-39UTVxR{Y=bM2M)=tLCIO)%HCD<p0x2!X|Hvfa@4g!kPI z$hP~m5gZIE+v`<|$mX*>SdfJrwwY`(cRn1mV~HJhlFjZ>`k@6-N{1{$tUQIz)rL;; z!yL*Jq}=U)Y?rGekv{_$@Ch;AKx2+ji-;g5Q9^5qb^w!<#?JhGKCq<vGsp#ue!y5L zn8Wwb_dsDb0WeO26IFrVkAK_YDf^yNW06D5LifI-p6}ukPBrI~XMLQcWg5Mx3D4Z1 zvgnw{5jvLWNx5&EL_h-7kz-yg{DL@Iap{~!NVZ>p853?|KAcjx(wqQ#US>5bm}KsS zbgHi9hI7AGDLxF<CiViQ^>K`}%9v=gRlLR(;x7N^nSKc2Xt56Y@#Z+trZ|^Zy+SF1 z)oOpX*C%O^hiH4P;z=dd?l>cr7_y$O$CLVG76%e*UWw3I%9Pky^$wzT!%o?zo<f)9 z&{|S|qEEcjlL)z2Gqy9Ssuhd>h-krX3o#KJXBoSA-<q35>qP=?02`-7MM0J89w>aJ zFY=Uvw~!&!_2&tYQ7I3)JYYj{E0C0F=0GfwvUOx7i}+0M6ur43d!9O|avV2cc=3%O zA=s=*%ktQQ<=ea*4rO7DaR`NG!%-j)J=6w&rH1YJk6C<)$67VKHeRcGIT_yBo1Uq} zD!5x=yYC&S5K3gu&SH-RUypl1m*yWGX6v&lA1Y^ih(Ya5A&WI`$*sZ~^~MijAERW> zLL53$-3FHj;*L(#(Gs0MrsyORmH;(mNiXNNG*TvR4&L*B!Q7*8S)`Oj{G^ff#Ozdm zx&0!r;l&}fgWXiU(4p;^Ek~<=c<Xi*`<<q$-JFQXDrHL=A-c(>9foe_dQ6F_Z%WYj z>_l`-`?#TD4wP5hw|P1%rQh`xmTh=hp4mmW#uQb4_{Zj@51M(y)NJTvcK^G#tkhE6 z^gp0@BXTU@W0D4cGQ2~?&AW(tCv0PXB|U+fHniRtFB5{T^v(H5;N7k3FN40;;CW1( zK%9Xmk_a=QtNnnr6(_hX)(6ZiAC_S{&>Vr=05w3$zo}34$LZ{4ZGDC%t-70ci%U!p z8Xec-<Gfx8qtEzuRhbX4YGP28A~;CLUcfEUeRUdWZ9Z`Zlb<X55~wO)^CR(=e7xJf ze`3Dhe{dxXEgVkVoF;47Jbu>Mz@CGRl_v;B__Z`Mz*6s|<K>BwFRkO@PRg%M?R!xt zL)hD+!x*Pi8W-AjqZl{cTNS$*lOK70Ja<~D^WsvgZCOmbk}#bZbyJFD8Z$Zi>+ljC zB`(yz0;h1B=Vy+GM6VVuYhv<20Enllf2e{?8W_Fc#SA#Gn9a6fEIgZIbn%^$hojmq z2Y~czuD?@xm21Twz~G@@lh0(VM)M%w%(KEjNLf9i46>M>JO&In;*BT|qu1pZnJ$#F z@OL6hQD2An@YQOr@+Kg@a(Ht)HMe~!<g}46c~#P0Vh}tzp)pzD(kZ){>G}APfBsE~ z^1d)$ilppF6G$^oV`8V09KPLP%1OdkB}rgT1r~$O%cqUci2>=kdHr^4n#uRiWO8cz z1;io~<~6_9HqN0cW|vmBU@RUDK>G7UT*Cqc{`yl#Z2Kc#TeCnEn}>;(4x0Lftj#^h zrm`$bk)zbFq!mFgSXYVoh{qRWe`j!`v2MWDm}w!p`-yG4wESIf^)OdwT`RFEbD!(I zFxXZHRst^4A{WNJ?)Cj-Q-&v|wF|znJh?@9`U-`pw4C~`lI;Z(+542M$(LtsQIf@( z_*q=8mSyX)Ij#266l9pyZ%`e$Bi|E}Sn(`Poe9~AeOx<Y73xp>z7Inkf5ZBk<rgxj zvYOLmdPZ8i*nI767YnfRSGlYvsIvB~)G4Y5Cq_)k><E9Fayb^~pZFASSf`1YQ(`@Q zuNj4cgRD^P<>OeHn3yKy<y-%Sf6nKUbk4#+JcRRwvef6nlFUHGqb05*A%CN`FYy=v z^&I$<YxvNYwaOG{x8%7Ne|gdB86&9Vjr#83vWft6mk9`jzS{K=A-sH>S5{k1(+$M3 z7ZDbKpDWiXWaykx9_Z~A$IpJZ1DP}-!pmL=o`UPQJmPVOV;EA0sNYk_dEl7Tl_`r{ zF_Xl9Q8S#~?7o+bR3~2GR-(iw!I8SZQ2$)7Tsxq+4_hBCX&yv&e?q!M!nbRe5YUS5 z8+V}pXhKML>@X8x=S;?bXH52!8XM?ci2-C?4qJ_b;|=~L`GD7znvi__Ra@H3DI9!@ zjXig***uK0kb-!tM^(^Uk*>vw=61$4bVy-x{_(m=39bE8D9J-q@8t!E=@5SO&XjKl zq|oz$OEZ%%a~@(Ge_<(M^8{*&<7PtN{C0yEoYY^OaXh3Hmjt25u%J3k@T1+XNR7R1 zsOSz^75G(=>A0w(6n=<CrQjP!d<upC(&tequ7sC|dIpPCt}QsB6KvelnHK~<CasGr zaD8Lups{&Rpyo-DSLa!3COv+kQOvH#cU@^qD1@gn&<95Rf0i$^SwZIh-fWyh?%d^4 zK!r`X24S=(%FlNrz1=gX$-ZFudY+-7Ka7t|N@#(vhup5c<Wk)deP+yqitT4x=!^bv z7su}qFTRb0*2;_Tjn&!<ojUjS*0Kf@K8_vejr4pMgV2~qNg)z>1PI+Y?(s?e58SSO zi{UI*ir=5NfB7t20VVJA=ik7g5RgYegD(c|6Mj{vD{Af1<EPQH8r_o4-K@>#O)ffC z1{IG_IR1D=iX0D1h(aRydSbXz<1&Ect+N8PVt4!%1M^XbSuX~ZNQUddgyx{WFrLu* z<nEPkec1&)A;6qgHsdIHEvmWQqrV&875w|y>%lOoe>8qW%{5C!_MWlYwVL&m;3?=9 zJlb_&A4yz%Vb92=Q+mL(#xn{FrEP`x#Y)|~a47!P1yL+V1IjrPypg6-eL=n9X;YB{ zAnlRx(uxD!N+tXn=n!ayezG+7La*W<3rEQbRJI}9<hD^Q#q2HiULHP~T}W1nB`%}c zJcwl?f6@J+6HK6G`LKR%QwOqm7^Ym-s=gkU(K?QE41~Ye8MM_*2q>9M706a#@<X0( zD4o*YR3mdexxJy(+?A|%l0_LwS@N_@niZU@9%@@`hJ=TH-~M=7mEZr{acC9Q)kA`U zV2-mc6P)<?vIrlVAols8XnRq<FQ(Ry8$?<=fAl?t$xkd#j=#eVif+#jB~r&Hr{!Q1 zvO=E7nm&XwMqY5c*7MdkrilWGlVJZ`I;HAR5_`3}PdHC?tZ*K^skOT7J1iT*6ym<s zuS>>qkE^(spCqDSH9k4s>{1HRA@fwv3D7aZCxnR2KV7<3Nycd)zCAYP$<ZVtgvh>6 ze?&m@gD}r5FpX;ECoVGdO>*IV&}46z)75-ui=}eJtaCe3T|2`I#B=^UxX;F37n#?B zf6BB%0Sr<CYK``Tgihx!xJXum$CjKvy>^C~W4;v!=}(eMED}M3kDw+QX)yYsM~vl~ z`R5^}NlSQ4bE`2l1H45m*6HaoRB53nf2%=JWj);ziNgI_tN7`hsq4k*G17O#Ns`(_ zX(^!H=di63L64Nd(Ncdq(bQzs$k!*bI*@ke_#^CPJ)RiZtb|u*ziadixA&uL1(VB1 zgf-YjfKuui4n`<oDyiP@vbg@EnvW6<FyX_5$m5=7m?Q~2k`OSRr}9l?uur~se>NmO zdCYFLV#aCMV-Fsc-YowpH^2R;+N8@Z6_P|VChQ!|*5l5klboQF=&8+O?&lWnQMbT~ z3Lcpfx4Sp|_AqSz8T@vSCefjEuF%S0c~v$*{g6i`rN3@{7gSXyYSv~3^-GTludcni zP_ez)6N{&Xc|?(<8sxQCoT=WYe^K9>*s_&~qh)E)2HtMZ|1&+P@d<|4dC&ju&i#0; zKrW5NI=~q+DCCVbfKG;&Vo0+Jj6coGE6LC@1bz>6(Yn(e0_W^A+(E3Eltu_8SX5c+ zy-;QVrG<S^`w!z694n<CJi}Xk@&?1c_1dQLZ^;AHIV_3>Gi!wINbf0ke@j`n%?nZi z|5FRV<mu@HnvVGXq6B8jJ=7SY>Trx>Jzdj~HWK(E8%G)d6Iy>9mfUq~br}W8n&>k4 z72+PB22pF<Z4xH;q`vFYRDGY?TY-mMFwT&g4j|8Q0iWRAxV+5>jin0p%mH(O6%&Dr z{Gt!iS|{jtGB>qI`#;`ae<FOkJ*RGw?Pr1Llk=;)Vo#HZ-3(!+0FgA|HJ(OH)3H2U z=ijh*pQ!-_(EZ))HBiETZS2j-P0=yq=~nC-%>_BnUeG8p*p1VUE>YN*1C?WsX@@0k z08I*MRyf1;EWwcRdp?{#&pnG6LnUwKIG^(=xRo&S--6FNoJ+D^e~H?2O2GImFF%(F zZCazH1GkoJ2^`d*k7gL4iPUN6ZYQ7xUhuuFHr$=H5o+^{0*)QldY3Z3u=NBgaO>KI z!jrx`**q3TILYLO+lg)O`+84i7*MCq&cL-*MZtc<Tf)IzBK8nN5;Ob3#Hil{{%ljx zCZC4Whl{L!pj8{re|62M$0Yg?U6cMb{=*2g(Y8RKZhZDTz{{~>{WZ~(Nhux<yMUw) zC;yDRkAvQ;yk-q0x<&PNRknPVJ0tCiDvO<y`a0P@o)>Wqkjwd)^^sULtlM59@u?<n z2SOGp6-sZFmysIE=gLlXI#4vVpm{MCni?EivRg+*DsER=f2Cm~A7jEiau1tztY>zM zlo)+z7k-Utk{0T?C^izHz>)bbA(pV3=Yi-0^G~SFf%SZcjPv=7;$(_BcTk7P`7I{R z#%cfUq~W7$3pBQ)=}7<6W!1Bk1-zDTD$^qp@nnTe8owvJ{KSh)VnMsvMES(AK(GAK zQwA0Mz*h2ifAo#|3!Xc5oWiuSqd*Aq&u3*dK;~n>k|XWSfQ!MtCY?>rp6AdD()UH9 zY2}xqOolT+t4{JUUTF<`wTGG62<$?cOTIT^^~l&W)9y`2KqEsPYQ61BglL>5bA;0Z znYe|wtPFzbsculm=LRL9=MnLo=+dr8pEU6^c}S9Xf0Rk&uI6I|`ibo4T2^s&1M7;A z?~gZOOLhy~YRuoCJzsX~i&CeSX7Qbm-v0FhhP^bx)&4a6!CJ7}YPE!zpkU@D&}&N2 zpvrw+egqG-!dd=-;5(~V(y&wv@%G_1X0@f5mT$NtMLjYvIr0FXbB%5BMkFAdq*78a z!dPRfe=#npPiD`}O=j^D99}nW%Pc?xFEClt;i|hEOTX|=h>m_5pvX5a)r&UTMywO- zT{Hwpg<?SHA#FunFw_#VI>EcwjHhcNEV5(4NFhefJN@qCr@}<gy7>!uqy4z!u5+bG z26o(1^>}w+xh=U%5M#kj;+2eDTt{AVyqNGxe+e`#OOfKYSaDx#J6c`HB_<|FqkQCK z9au(x%{|`dn|=B9scQM*9>JS$McIrose<b&h{cW1cdUU4(}}@P)&oTJEedtbW<aRG z4*Z5%vQBh^0vUO&qG6+}TY=5%kh0Y>L!%(Z6DqeX62O$r<VQl-RBi@VKEmira$NFz ze}YRS#{Dp&v8+OjT}3_8LG5pr<#Y2@X>y}tw~Q7apJ_}s@!NeQnAI9!D4?QsY_jlW zDzVyu4&dQLbCSv-fyeSr(W0#08p)#N<U~VOmNY~#$EQP|t9q#7`1Zy{bN-|NMa?k7 z=t7xjjt`!@>cHoIyBa^!d&A(~6Vk+Mf1n6pCekTk#pG4}Queb}ZVjSjd22_{7rvxl z5z~iz$T4;y$Eja7GB3wNvSl^ES%3zq4=J8110OtCC6#-$ScSf~@QXv)_O)HKm`J*q zO}qfyt?W@RKvP1MTh=LM7<yiOt5zL$X_G~(ChSP_{rSULN50(G_MoCJ2_nm#e;J{+ zt1b2*CZKC_tK1}|wpusFa=5*-y-}YjpIt_0cwWG?bmMo+fJY|ZJ%c}ST3S)@qk!zs zBnK5*eJq}N_m_({MvsNJp|mv&_ey2E0(@yt@5#&@sjPVWZ>f=aLuyD`64LmGLhA=B zd=KMJ$K6=g-?O6ohOA_5*{u<tf3F8+n-gJ#Os+q0dwu>ghO9RhZmpy{#&`@~9y}rC z(R#?t8fE@8md{-N;7dg5H&P~{6<;iOtXE)TljW2Nmhs^qQ%aAJzh6RxS71SV3xC|# zH}q3F|9wG@yeu{elASV0a$95@tY}C&<@ouA7=4AFYB_I{83}3$0B0x_e>oj0tIu77 zT4PKI{5&;(spwrC6%@eO7D&_=8KK1bUz&I?k`@>0Blm2bSr6ob)aup0Hk%zNC7&fX zV2KkiEPbdUqlZMZXq1f$E=S~H#%^)l_)Hv$yz?bDaRA$?QI0=%@@O?cu2efNZCI`5 zNFM_Wz15yBTS3cn(}ba}f2M<a;aqq_Q>KF@_Mrkqx-j{FJ~Gg$CLzq~XfH+w&C==q zElJH>fg{by@&ux>YoAyXQfP&GDj!p>An%mWbWzF*pfZtlTbR26Kf-92zX%2@Y=3?- z@HUz=o%sMhqpm?hPpWQ_l8-+0B8AvVZ3Qn*&fMSE$F_Jj4+dT|e<0IeHLt_GWA_A} zRkL+P%5;(g6Q2~9UPlUTxV}`Ut^sD4QW|xj*s16?629BAUUr*M|7-_5$Rv$uzA^oK zfqeTe#3CDAfXBuhawZG5HUHb;wP>?JoCCT}li2GIh6w+L1frGk2YOlOpbi+u{bS<n z8^jp}|6$MrAv?^!f0fL`rV)l{VB2ux{o9-NKYgK8eP<`aDE_(IA10Ol?fZ^Y^PZ~t ze}f}kWB<G;^_4^s`L_a=;)sg(v*JWCeYF1qn<~nBlK}kxki;;EcF~@R^Y4?>gHoF{ z`{u#~y&vi)(U(%){MPbbRZVU7qAFcoRkiU7gyjcj*M%#jfBVyRaD|2aVB-TM{&N6T z4Gl4djPXnXWIsPYcQ3Cj>;$qK=I)1QMn#*7`hmkm3S75qPl5-$7(+W;*U@_}(~1Q< zRQw==iRZU&)Ua=9IyT8z$3^R1X%MywUr3;HRhJFD&l{2|tJlk1^B?boHj4vD%Fplr z%p13-!M2Zwf2!DathWA-9P)Vo>h{GNGjt?m<WI24<*DU54U*srJxm;GnPSi7h&JRZ z*Vk&^>>J{Rn5K@=ZXX}IF4<j^Hb5|zxc(F{Q&byX$e88sL&2EtS9ZIoffBAY5!+ZP z_QN47#ma-%vxbfd$;DAA2hRoPGNz+I4O4-L`g_K+e{IIs-g@6x$yC1#ru{#aXdjHZ zUOm8ACZhSbC&3z15$OBC6GE?1U%x`ho#!*PTc2zX#m(1RU`Iqo+5lMm7)#$*+P*p_ zf#VCc;ZnRPM>d*;Y*X1pRJ<7`8pn^bOUr~|Z}eS6-jMn|y<@iCUf(z?eZgJ8rM14> zU~7JLe}!5sVN`<tr2q_=O)0FifBH*iCcn+>aM+eC@oz6#q%yxv+bpfERV_7gbmaJb zVD=9SL-5Lwqs8X5TaEhh1FHpi9GH%s!ZzZUxf~GpxU5^ziT}C~(wSx}T3M`&qpwEa zV0VBUO)caXzl9>h=*+fnjo<n`6>E=H!1GV1f77-2qm0ayb`XT(FOw_{5(WTBts_{~ zOvuQ{_MCaIjuvWSpuR9iI`0mb9<0r`FYhhNSZ$5&RxWx&zqtER&dA?zUMIG+?)_PD zbn@zt(ymvFQ8a&h7spgY`1l;M=Xx|BU^EcZr0S20i`!-Xkr%FkY@k8DnyiH{%u0uA zf3!bEVJmk<;QWFJom^Am>M{;!2Qz=WFwrBZg+}O~h;YFrW>>FQ)O~aHfNOg)Eb8|c z>|6GajP;SUroYnhJ6|VOwK|8CX#AeI+F2IL`nc*H6zo|V$?S=VD?<6q=3mj!xaIA6 z5JLG7F_=Do_HVS)b)`ByX{p(|@LP$Kf8!h){&IVma*|>x*mLFFp#fTLXf~Vvq;?(F z=?g_dLK0euq3*#%PcQ7_^ZXWG)3iX-MAn+F3}#_vj+GrdaO$#|kA_sXmfA@5wJJ0n zc-`@Mc2gkIxHY819yE(mvk;UFs^WI}h6`$|*S6NFa%Mkz2`7lx?*)y&izt76e|&S@ zopJ%JoHObkZ8feUd5%TEcuOe^6ZQsEk?XDYvC_&7#qsY3Ih45nA3Yd#Feq?66s3Gf z*q_qIK7aXwv&%H|=*UeBzr$L4YSH33ckJ#p7FlG;Adn}G<>amBHY)I$&8_&C32W!> zVak?V13K&T{m%38mcvNR#KyMJe`>ItpEn}c+7lihX)N-I-+RUFm+eRUokgMbjWlq? z@qpghn;7h#J~YA@!T;fm9O^yw&z`>9YXFIAu#8*DcGjcwE=wEspWVNCHIMSI{E_=@ ze7>o#uWw^7H#VelGglD>$i22oS~QyvK6DsnDC{&o_#$w0<Z`wGamguif91R3JPMlL z>O6ZfVx#q<{K3QkPXyaUC!Ug$Od1-ygO@Y$<>+pM?rXg4*q<T^IK%!q2s-;vh#CHu zl!Rdj0z?AgNH(8R*^ML{Vp!j*%gf7yVpy}I7xcS=-cI)>ooS8Ja<4Zp!Gsdn{1+W_ zDsu7Oy&N0mk7SgS!Ebmvf0{dg>ium(zemEvNC!qoM;Dcq;lMMqut@s)3eJ`5xRGzl za(_=xk8;@R4_07*7ybtaKPjQ)Ok8L1>vEfV(WLlf;q9;iw%p}MZ7#37bAeW~fV*WF zl$4Yh<PKo8_CPjrFm+lKK|Dchz$POaG}b&LyZlpn&y58Y)GrzTe{|q4ORZG=zs>HW zu>QsNP)_1k=(5XMHJh$;ml-9g@Vd@s(n6m<SQf6uJZttbpSQv$(Q}a?DO$6UfcCm= z^j<DlJh{;~lWn;>kl*{3=Z+#L(;4K;kevQKLi$L82r~>0YWpbyj&X|kZzlKC0P6QR zzz$z}R@Ts2%izj`f81}b^m)8H5Xg1PNzq1e+`;b;d+FbHWzRNFgp@Fsk1m)<=w+)t z5nB+OsPhGZt5f@-OTJ){7+7GeJr+`iOT_+?gsGMEuh1^nin^ltb6R1-I9LSy?rcH$ z|E&_5^~<{|HxH_MZ$NMaYI$nPPGSKk0%+U@DH4v)MiEb*e=s>5=R^6+uc6@8S-IbW zI;cmtkb(*OyCwWr;~nj!s%6jI{p|d`UWR6&Y>x`2+S<%K(#CbxSc`+MYsek~pMx;m zCJJ1+LiAOv;U8i!+?tyC-;~73pJ4dWSY4URdg?)ziY?Rcd||skZbmFhBCw9o3dj%c zv+{P!4~`*BfA4)n!9>XXZAf3uB$)rhIA`QLkNq30v7ai?2twpTSt#Uw^mC@kmGRLQ ze{iAvJ%lM@;}O96sR)Zmu*^U0cwl(v>Y=izTHYJ+Q<psHw{WKrV<Sr3(bxo8;p<E4 z-J}CKaU`t2dmoHg=lw{jeT66XSrht4E=X3)9Wog-f65N@0NO$o{<^~LXxAWL=D-;C zmj4{o)8T|urGj<**AY@*tQvT?XF4b3)u!EvdA57BR{NDaG4WsUziAuB`mgPV%7X&a zBs^_Wb!d3o(vLIep?{h5tJmjjYvj*v%4+gl#GLmy5K9EQP>8pDC;u7S`|~61e~NHj zy7T_he{VyVeHUXOiF{435u@J2&v2l{i=fWn#0VtJv~`5aJ?iVefgAs&8O>>DwCnYm z1)EIZ9<4bspXFhlr>yocSwC|zj_EhOTFK7S+sOA`E56ouMNoDxrX=d)3?gNG?Pv`~ z#j_zo<qIKciPpUpJFe%>{>vN8`yVhB6Mw5De;y(`^&cw1p<Q>Y?P<D*CY3rr(nGS_ z@248apYVf;+>{@{{V;X=SnX;kEkSA9yl-B0{4;JsytU!YqxEA8o?lpDQtwtOV?o!K zaS)aJ)bAx)7RH&EgwszwxWTstZ3Or}BsZZQ$Ft_Oeuq=HBkv7*`AR?unwOp2J~5_Z zf6#AcaJpnf|AU!siB6sBOjO9B3%A?(qSyqOrBkTGZDYIx5QWl!Y^a+&fJWpL=tArs zLh|+L4#qi5VB`$s$I?~H_xjc7gSWnxKrQBPK5}8|eU*a26Tm=V<^r-#9HS(|=d_y% zwd=;afx5eif)#UDEItdI;v}5k=Ke!8fBFIA?<Q0f;=eXGRmkG2<CMfX>wXRb8ZI)w z_KLfzo*5B7)3PU=`>&PCS{b;_BEJelu9md)*yb`_&fJ)xT{U$cKL2)RXUsDX=y1=v zVD(n7d$^~othPOK@_}e##tGKqZae&nKK#d$?Xc{2adLC0D>FWG)tPU9cQoN>e;5>x z4-cv%+}1k*oHt{am8fO5;KSQ3SG;`)W~)}(X+QM96I?MH#rOLzC>=tiV@~4Z`7m(9 zV@jMg+DTK!{h+27I_E|Kyy;dG9jlnAnUg2oZ|5pOK#*r`6QOGl@&p{NGRKzOOL%g) zg79BgtBHiF%q!swAyjk$%)FvRf0&A4f2apa*6=^oBPN_BNDvaBvQf@oU6J%NfQ22q ze?y8yk=A#4*8NH;^loAB<x}g%wV!@Ze44{!cy3j;Hz~l&<`bal*<6)L52eMA67D*! zb5ZEs=V)V+Mb6PKC)f|?;H4P40r1<BS}~ZMNa4b*Qd22DJV8Lv&eZs(f8Rl}nRhed zjyM%lxM%V_AESt?ehoD|{~h*2zJBH5r2Cv}VvXLorR@U$D)cc`TTOd5Y!WLVbZNfi zTL+0C+tk72ozfp$iISyeD&F5FV;C;noj=iYvq5sEZpUN1*xbuR4Y+^rjA#KLn>O_= z&gslZJ_M6tf$CeTe)o`He=Bm@%N>?&`#tj<IVt|~D^BtuiXPw9#C~W{Jn?p(@+ep& zqE_lR|Na=~b(^zTWqz#HzW~xHc*fDvqI2dq@6v*ZZz2>vz{5UPX>EC0o0-U0%Iv_D z*77P^)Z{Mi2wSwE5)rjj*ew~kozM{j$TAnI;e|BjwhZuK6VnLLe+?w$3=G~bhP^?D z;??!eGSzr@J>ioI5B)>r@2frrDvYkH`%Rg@=JM+7TZ-9V+y^jwKR84)Gsa8<87Km1 zM5ZKv8)zBEo^!pb9ez$5(l2GaRW(F82RM|>lo0Ekqv277ZvSwGN@@~3P&ctCsSf-T zlgZ@iWw#a(I%^44f5;2*bk3*c`=`f`y(W+xcd*suAv2TBG>WV)?Wn`^Pp}q7R&S0; z^!d^9_ceRlEOglGa!nm@P~G*|{1Tq#1ywp&I9()Gryq5IhKe4EDSbKDhsn`bq9xeE z_dmCr|7uLqy^S3SodqQs_Q2!%)I%2Z@4^WOg+5_Z$q)K>e=VEH@$T$7x@j6PE``?X zEloDMHr=g)=i{>6F^jhf^@oz+tw?TW=qOA+PJM`#R6anUri~RAy<j6iNrWC-(p=pB zQ36+1YS%Tq*k4&7D?jwP@AWtqiti`JF~-|)7&Ip;*?7^Ff7l9tM|tu7<$b}dr%NHt z-Dl#ws0NsTe_Z+f*k6UlUFrH5#rIk>YJ8E)mMhh7qYu@8x;)bMSu8F_kheD5Cf*Tm zqqQJBC#x{An-Gi>B{H`23ks>va&8<>Su%kvjus`pKg%(}ULIs|tq*Bovr(1G9yC+t zexEd_6CMYdH?WPx{x3NR!+26p>KY8}x>*O;tu7c5f05ET(o$2s)-vs!9mED*J-f7M zIw#c`vzD}{gr-U$yQ5I)giHehe`K&mvWg@#un>t!0V7|U=r)3uQO-@wU@tpZOZ(7q z>*l9sXuU==VVo-+PPCf{(dEVlc*LB@XRLGH>kItK*=oZi-_@{l-Xs188Htm;6^b;R z+IJ~hf1C;dz0{J(mr7Ofpb52;#WWifY1RUWgLqo4)o_b|4^%QjzA9A?9*C}dmQ`jW z9laWci`?7MO-fNnE%tYB5I_Ru>aBX3Az38{9;hBc>2#cUIT5&!4u>N)M}HELP&62^ z-!9&zn8W!GMudS2lYA=e?2XG@G2rI6l0Tmwe?qc^KCtXQxesyTQF@%doqs-1iHI() zU5`m5nXi8UI3yoY>>3RgS+P-?V33+I$|h>rxI1Yu;Hl3r<n-$V=Wmw8`(+49V%}4K zY)1Tc$=G}>TSN7~1b`9n3?n{(Ka!^ND9@wbrk;b4L?M;GmAr;7G2Upb)DGSdhJc-v ze=fRSaR^zJMXg5KgU%vk3BsO=WA*J{hun?(HJ3+oYF3TE>mv_aHkRJ_crYIv$v-{r zoquNg!57H4PLKClLl%c9sX+1{Vn2Aqz2vRK0lLV_t+d5Jc%Ea_<n*swwnIJs(hND` zv11_-_hdh<S6k906$=SM%dyflmIu@Ie*@V~7pVrvlEXsh-$!$?(<OgH<LStYDBXlb zWo~pBlH~V0qP~h1YUH2+i+t9c`9uFRDe<)8|G+>fwmo0#-2*G1VtYH}Or=^dZ4-ri zW85Iwz3Yuq%+g{^wZbZ1ZksXUoTgc5IdNLzntdl?VhTH=Un&Pu2WeuU)ZD2ke}in~ zb{%fRlKt}!D1NUa@kG~@URkE0a6_mE`b;9Z=zs79!wmmQaq^}1Mmr+60q}Z?MdQQP ze$Lj&>AC)GdSTVUaE;OUIW9TzonIlgb<dB(^~zaFA!b6d3|?$71pRpN%Z+bfPq0ps zjU<Cp(C>)-f%3QGwIlrU_<slbf8Cg*ew*EouUWDMMn4js-Ql!#!pLcpzaRPhw9^#+ zdARnc&n-(?^9{eM-z{tddo9c0Vl}FjFF$_1gdOxKMz>6~-B@Nwwlv~$fvJm^{p~Qb zxkGc~5VgoJ=|7G?78<Fz|2q&bK)mzuMflX7*rpogBTLj&MetDo7d)B~e}LFA9vmy- zh1>STYW*}FeWqEy+#g0OLc<5_PnEzY#e#J`HuqK17k&HGqG27-a~Mr){Skj0X7Ij` zkUspM>~|1*a^GdX{U;nOnVP+lWYkTwA2o}o0T$MWQS+5{!Vy0&EKA>~kjN)<JW8Ct z(<rFR8v4q#{_|+4rIon=fA~3Td`zS+-&)Lop5I<_Xis^S@(=3zsPNNR?gQSl5m%B& z-gfUswOfH<w+GYW2&B8BkhE(j^kkiqFEJ=gNna?_j~wYJBd%~uqv^ScTvG)cMTdkG z%%t%RItqEJ2upMvRK!6i7xE8-Si065bv^Sn`VSIkgpI7Ge!UVof483j8{X3t{<OU} zWT`e%3Y!KnO9rSV`NGx!V(DVv#jfq3Zhtit(L!BaR(sFe*@!?*q4dW(qhutLitrB$ zUadOMSzp`Iy2HJ34*>@wYSL)R*G3%dMOWMZ!^b~H*ZF;Ipm3AMwr!g`w$s?QZ8u3{ zyNw#F$qskisIhI^f41J_ci!{q|BUl>k3Gh^C)T{KsnxZFA4soD1q*{hvvh+4$1N~u zv-#}2zLVJGzv9GXWw5Rud_HW`JU~V|`FX&ksoXEv|I6+?ODhj2W0g)IUo%%6S4k`; zKM%4Pi*~Jf+v%^3!)Y?$NR&#dPrM+YZxC;1Ta(F+5kITze@Y%AMZlA97t<f0ZQ?>^ ziv&5?2jUi}TPyyr!Ewk)#F1f_gTp=Z+xtj;_oUWYFf3m<b1~zuOwV<hqv-)dShC<P zloML*ILCo41cE?xYQ&L$f$_O9;strjpgaOUr&Eu5z-pdb9!Ocf6Q|d;pbYhYtPvrM zfHHCDws7ape@VwxKa_K7jU3zEROkB7txf;^js8gUl}xu-1$>H1UO?Q9?%8f|1T`L! z&^7wu%(<~=Nr&Rhv)VIWSFk_~YWJecVl{j=nyEWx@!VMU-L1`IOKqjy1s;!pX>h8N z2${$d^5Wff1Y(e_)bs{rVsq=6qZK7M@N9b06!!m6e{v9vG}(l~ey49bX6NaJ?9uw- z;Yoj=(ahk_t*AKsvv#bzp{p@fRDW(NN>b|u8kn)l!nQ=|5&R!be}a<f)q43I=>OQa zN`z<hXvC(4fxFe8A*jcgoLc6*(4r-U=dalG$I8J??4_l5KW(J=W%0@5(f3lx1otR@ znB7=PfAK5Y|LW?4@(BWoeL|3~9hPItN9}9H$67pK;+mal{2Z|7Ry}yL?9BV69b|f> zNS_Pq5}apXR{<?Ag2Zli4dO(Z8z{D=+H`<!yo$g`FE_1{uXehs_zpTP%5xaoh)^Q} z_Ec<J47EE-miFi*`{|%lsU`IP4XD35i^t2We|LVi*W$zoa-v%b03`ohHk_UZicj}7 z)b7Zfu4MNNpWll16YvZ<h>B*Mb)p25|0%Wu`#2Tg#IVBGpVNRt<qDUk#<>Be(IGd+ zMbAz+PRK122Xj>6{E>xCHG#?a|F4()L9?oK*?6Vr>fFAPkxbxY#Z7Mm{J6b0-4A>V ze|y3dl1rm(FufbJG(ISr_r%|%R4<+h=pKp&=yxtsiHFXnFbnUl#cx)tOJmZO<?=G2 z*C}QrhjazsM-5#J#hJ*s0Qg>wr)cyz{Dk+VeBxQb{Y)rK5%3h!|BnDdCe?WkpHlU9 z_U<v{;#Y(i*Z=qp8ZKYc?^0%g&@gz~e|FCHIw5dAq}<657VFf_5B3?#6dVfioiGky zFL+fV5Y-ea(!E)2$0Q}d_2SpubkV6^5Wdlt*Ie7@Z~bgAv$O+*pp%0=B4GWj!QlAd zW&9r>QTTCd+_m0CAZ<Lgi`=lE|CRi_$i$}VOtKOm&4*mLzZ*_6|FNVTs|Y;XfBQX6 z=!Uz4Ajo&3$l@!73j($px&;ZaxO2pf9nEL|5+QW;@%J+8VXCQICw9t#noY!$fcze! z>7phdP9^exp!uZY@3NUV%=grEsQcrRK5|}qq`4`78lS^lW05p2=U+!;?V{OD8xWa- z&;AGm&Jv~ilnvNNai@!SSqt}re+7M}X0rJdyrLzev6T;A-yowM__WsLYgvv;X+Q0s z(k!k0f55BO3y@43CBS_hW+z4=tgeO#($kP!PJ<;oRz?QLxko?aX5qSA#t}jCJZA|< z0TzdMlSp3ht)aS5RO9p+C6zL_$7EtVQhVE1g}!v#)dQ=b>kOk{F!)F~e>Ok(BgM_{ zj^Tj)U$?VEp<%WX`9Sl*WI`5RNL4005`Ylb+o9y3OYkX;xUYim9wP$(xrsC9pwV+# zt3HVf9iu6oMb?doR(|K5T>#o$936sURDbrilyo0?q0ryP)~x&v3iKHZDVG52X@^42 z{|)X`uz&n3<geOdEd<7Vf81ggI&MPdH*IWE80a(Nq{MwCj9apmFog7!jXr1^0&&H> z`(mj`MnVo+3~Y-%yQ3i`9wC#mMkC^Z(f<b}i%<lgU0zmJo}lw~>0bjNe59uf$2LlL zhpkDwH?zvpu32|>^%B0fg0`e^c26hw2^j#;eHhyq;B>@6!Z8x<e~pWL<6jEh*=xSb zLBRwrZK_oLJ-g&HM1gq#;h4vauI+@Gt#EWZEE5o&3E9{EZ=HWZ7ff;&cwJ&Ug7e90 zjMv_earq%hNfWH6I?ttgkxt!4=(jyMFZ^LJ3*U}mq>g2%>|v~h`<A>q3SmpdFIs=w z#|V(C(*@nX+#H7de?HHJ)^EEp6s;Gb1=QbY<8L}Ld{19oUe|z!0a<v2@m-vGvbAP1 z<X5^Edp-OdmGJE^>YfP1aeIa8=`^Uf4*JsS=zrUL0aELn9|)-NaF4iWZ&_MCHoo^j zt8*r3ZA8CcUxuA1K{UJP@`dG!mz(P}laVm=?a8|bEq9%)f1)<^|9F1xY#%+bi{kTg z>@TVD<wJf7IBCuD&z=7N6Yy{}ty`VJJZRB}6ahh4Rc2h8Vm=4&3lNG=*c=BFANKay zJ9N^OBov@8A*9zzwmW6p02O{_KFuE$uZcuA{mciFH)Oo4^Pk-cfC(9Rk^s8sqJE}x zI?W)JX2Ug(e;~z&jO0D~m3S@pgUa5s-q^8+DW&m5Ov05G@6W7LT{snyu8yD3&9||x zi2!mIGIHXOscWwYN?bM=uVJn()fS}gR$v*<Uyu#%6q3j6PxQsFGtvjp@<GNIW;o%a z!AzvLMqiiU`M`)>9lY<V!hm*PA)rf)96S2EB#`}=e<nJ=UT@H{>=8nWRkay*H_>1Y zi!OMA8vWo~8|UjhH`kcH1KY#kQL`z3Ec)=YHd1%9gf99n$h$;1lhW%)0E><`+Vwl2 z*L=JsGFol&Edxs+r5XbU+%m2#L~;enkFA%eKfU!I1{mp4g`a=)4zFi1Uz-0FR~F*! zhl9?se_B7#Xw1Ap;-Dw{*kaA@$1r>`9hnLL$4j^SWx4IeV&o&zR^bUi2&vB%RL{~y z9=854Z*tAQc`{CdDpEux{cPPllC!aei@dt#?vY3kl6h!2c2_;w(^%7p)+sEuU01Pl z9Vp<Wv4VS#pmO<Nuu9h}iHm3o%AUX@q1^8Qe>WIcJBecEX;{VM*$_wOpf<s5POMpW zMh|KTvp1wV+g(}PTGP8!+u#Ik35B}+rKf5lgtrrLC)i6$ysx}5U>Bg7iAwxOQ=J?i z0ZxwTz<p9SE8MV=$&<AlsOctl@o=pP!m>OL@R4TPwPjz8PW?8#0IXEPDuoB*9v(L= ze}t1YF4%jJ#FNYU=G4zV(obbwkg>eyRMO~Dk3PD;5PD|>ERswfHtgMZY!*$Cx;zp4 zlS!`MlElE?td7t<vvUDvQUas9gF6VjBj>|6x6iva5;cDpwtr1$PP=mi@_>AHUfAO# z*`nX_F{BOBluHOcnBoKz<|rjisn!pBe|7u~BU%)jg)oble>>mnSgbn{Sp8KH0TLld z0aAP`%YVZykHNjlxhhO#TS5a^6nHfKyx7c6kq0$?SS)FF>;A>f88z%_xPBJ?l?+#G zM&Q99k3Rn0`GnR4uLw>p3J&uHq|pQlpkG^FTRJVzNEW1xUVP%mq0WUD>tj(rf52dr z^^%?IsoTZhd(auk=bF9rT;|vk$NAywJ7RZ$uxBNraQ$DrBM=dec>3i#T}&Pa?MlK+ zjWi#~rK)G`d|H{_t*nYE-<YrjgK9XP<IP?uIJvfPeqV!$c^d`jjN{#g&e(q1?5*^@ zoLqyGkp|-M;xvB4VJc&GpF;Xzf6YG@HHhwPFMmxHY3bHd{6RHPDdx4FkbklH_jAC^ zI;mcUe4}y&Y5wSbW9>GC1I+k{vQX-mg&x$kn|S~v{4@s{ASQSA+PN|l`aUdP`-tv6 zQ0ekl)))@*_CuW!?-`(+n&NADqn+u-+S+R!0_~6|=Z<2i^;56OP3~2Jf5bkFD5Bu} zulNn7jQ)IXZGFL12hsWx>n)(M;2GY5>cKfkc2wNvgKd+9biO=;^6V7qC}2wjS>U$E zM@3$zBo-b=3X;rj=49mE{az%Z9*!WVs$#zhM`8@5#8_weyb#nkukc(ir?y*7T$ha< z05Osyd5?V@q%7+{TTI5af4Q-k&HBo1IJVL0or7h4-!jztFWw$`oc|r-<j8P_2AiMJ z>LBf3y?5!hQ!X%-Ta}n{ZjP`shkMTzrO2WeN`waAp3R>3G&twj<hKQ@5f&%b84KR& z)KKYF`<t|9`|gu9rFD$ifG};~e&8T-9n)o4zGyI1RnhK}7ybcOe?nEMpZByMy3UGw zLq>O}1Oi;04B`FH7K~bz_&)J1s2p|%(|x1h7>?p_f%C|ASKYG5G*tt?yripFoiNuM z-dX#mN9<b+K$RrbC1{j?*Oh_-vF}><PL#%k*}T<f24#gU=g3&jxFl6@=S5{fg(w$V z?w;vi?JE4{?Io=af3SK;APPHbJ6Gt^#jFJ3phn?O?uEu_&~~O&GZtSQgeqxpr{9)+ zHG#hbMcDkmjDWQA6P-J#tctaX(7)}p)&xEn43+`|Dm!5}a}34f-g4aCZ>OePWRN;g zq!`1EBY#!hf~~!YWRK1~Jvy6r)UgI1|0eZ>N`S0qXsfdVe~5J%Q{EiHA$br*KV&t5 z2zc-%^>`(z^f;oukN+2X?K0Ws%m@;Eg02F<#Q|mF+r6CBrK+Y1<TKWh<NL!D=p7`a zjuoqtk7M3!JX)N_JS!*$6*)66Ru}takL0pH&^YL_pFbZ_J$~jYF?yWm>UUI}9gwc` z`S>lm`&XqMf3JRS6i0HgY-R!Ev21$Y=ptPemNi>OA#7B)f#^;A^)mIQ*26cA2Od9o zESj(+UYqt_`pvb!C}AV0Hyds^R`+R;)uZjuIsU`Qd<CQNe>F5p{a^bp`0PwDGUCoo zzlPm3%&~;unL53^AiQ{3LwX>?sP@Fo4{A`F!Lyt^f8mCmurdfR=_2ZsNkVIO?N^Vt zrZehPYj6SvmBqD`=Nit$stwnTa5y4oA=rmdrTqVEU2IaLl>Uq(CT0$vXY_-yN6-H| z_R!&1GdI5%eik%8)@ZQ3AjBVB534GsN~fV)=O-#qq41OQf@4OLDXf3BFE(`AAofJW zlAM<`f4fM?r@Ek7z-&?RuF=I#2M&t5H+&@>U={M&DyddAl@but6N09{XPncEGG*Z@ z*B!3;YsVXN0_M#U4HbPjdi>pWm(R)XjoQn0M)~Adi9l$$|NR_rvw{|mpQpF?&gM5b z^zw@Dripv|2Ix|}5>9)qoROjT82(^O#dsk0f0V<rqrZ0we?91R+IPctr2_8xLQT|Z zs+D^@ydw|eNhH}+aCA|(2J<fd9POvDX%TF2GgI3x<Q5r0OQHM(ICU3F0oQ-+tuGZF z_0Q>0gUW{e>s$znrb&~v-h}0UpM#I<q28W6alF=g_00Q%S-QZ|Qqsnc0{utbTjuUe ze|8Xfp&!d+9-QLqXQYf9`VA2e#?_h8iitkx`<7kwt}pgxk2^4bmZ=hl+|nHbox0HV ztZe;zehVQ^HYX95X_kp;nR+#_i~w}+U{(>}k8H(X)WF>Lw-~KyF5rv4i^B*&B1_IN z<TbjZ(=ipxWgXjeHp}ubD`!=1gK-&Re|uotv{o>sF%JUdSruMlRbw-fpF*}4Ch#ZW z*>a);DN4H}!gcbxn)yN3P5kuECGSD|RQcN`*+aPp<e7azbNZZH)PG%lB5>O^8VObB z10ev_9ryMhsfBoNhE)gj{Ze4liC)a>biqU&_+kAIMG_{`^djM<$+Z!nVC}R9f30L- zQS|}Pq6D8$S2X^u9K`E%dqBEC?1c05uQa?I_CUQdJ1hk~YN4ZbkCI`9*HBDnShJzF z2!ff>o1pbLpZTba8*oM#J#pC~w-MVNk+8OR`f!KJ^Y?|G{_W(XQd=$idRfQV_nwL; zy&X;c`pA|oXNt3WtQjXK>DHe!f42O(3Ng39bnM4PLyE*~n@?s;t_15+M;mb*<h#+K z^HP<<;xa$7(0(1)@9oiwO}*>8&6515I^T0Nx3<2w0Ck7xBk!r5&yuAeSV#P9gv*ms zTfWScnT0;L*2#RWw}764-*D2-`s0_FOW35Fd_{0Dp|IU|B4^l;rY)M4f0sg<^Kxad z<#4*Tp6%YGa;S;m_`IZPN#CfKEhFkKu$!l$66I-)L6Ki~;xNiax^(khgEoUPBQa;G zMgP=j@<8*<<8@Zd@pm!3BBqY|sQi4`Jqg_KK6k^i9FA+Xu?Jq~VWslE@IwwGv}A?t z;66Cvipb^B8u+kC`u+RJf1Q}7aS2s-S92#ulMZsm1kMO@{tAaabVqjrz8YP3Zwe!W z+C=WI@D`#0>8V9Lb;QRpkt|3#1oGH6VxQC;h7XKTPGtGjRUitBJCq+Z`KNy|kQkm2 z{&wMI;#AMpANDY=*)of`R&gee{=4ui^V(s&$<6$RYeL!r^7E1%e~mS=fQiz?)z0^y z`l&^)G^E0bhHQw<4K@OM4DZNGgfDP+gKD%UtD0x}F^w8R$et;ZNDqXk446kyJlug; ztbI1PS~U4~zT0pb7XpANQP2L_hD*7<hnAcoQJLq48or-80#xGdi~DOO>bK7`%kz^6 z%g37Cqbl7d{?~Juf3>A^!z44EQFEgW0YrAWD^MNDZw`EmqDXPfL~$Cb(`}#G9XPlO zfnj&O)W{j~SJxo-6UHsPTgpN{r1qN@8j^}Q+Cp>r-cD3a7xm)$3kBu1GBiRhdYc7N zi$4zn_LEW{5YJ}U(qaa?huKCcCVJz}=s<&3utD(0L5aiuf3F9rz#g8)OwN=z;PlAS z`w0~9U_meXw%|=;vz9%)9-Ocxvn&FBwklM2GmgI*5Atm-LIZIF&1gq_krcW5?q8}r z+}d0@D_%ilN76ijKCp4bT0YeF&@fFP=AhStRxLp5aVP82f1Fl4>kEC$POW#1<Ij4x z-pLZEs5DK#e?ctTt)|GsIM_%6%D$FT;irPhlp#cM`$x?`BbnI!yJjR~C@rl$T?B^1 z<zn$%J6vFBg*e|QfKa_^lW7~h67UzNpynkNfcz3%g~*upn<EzDJYF$sDq-KU8q~6` zX3xCdpA%ucREPJiai_}666Mo)wdKp6X(uM{%>i^be~6r`+v;|I!k4>)h>XkGGs$o# z$42!;_f}G)Okd~~T`V_O$;|MRb$+P*a_;)Fwr-u)d&ZF0*Gz2=z3^`GM6dq}3keWf zlSKj@Gti3<6Bfl@U&Dyg!8+!nTg+ebTc)WL#g0PD!jU9%&Cn*gtYpn54bz8HT&+B9 zkBKsVe{S1)RMp?~N?=Y^wTm_U{$@iiJjM)*_2_aOuBCEQ8JbfwT~t$m1Y~-r#^d<Q zmIDOQY&_Ti;|RB9GRjOhg)({$W?uz<+$z_DAl<O00`-p9=YL%p&uYp?`(D#M=-74x zn9jS}Fm<)urW;b`mY}ts=eq?a4_D^4+6gm$e<y&B*IvAd>tau*bA}X7&Q}{Q+?C2b z?F?Z#d_QBViYk#*E|!y*);*?6_Q=G#{;)a`ET6Q|#Kk%*HBQF?Fshw6viln@#pq*0 zeKDAa#!C+9;IprSa>^Tc?Yp3a=U+xbY64T<tOR{}oBAzpk&01|w^RhfZpWTRvcI(> ze_TkGYym@@GRlJ~_v=1$cT|LqF0q8y@A<AvTzZeOsX(ja%%Ezt{L}2?jXIla(nZ~N zbP%#u3OxP6H{ErJP!5rOx-Qy3y}tM+vbhN$xzFEj4D&!mO4~_X{~4zcHl@lTt+4wb z`M!idpo@7oG5;^cPqG8Wwt|i$IxGTde}8!nyx_S4+aAPQMtz~F01On8eN7liJ+%i! zt~4N09yDh|GvH#c+;VcW-UcoVSQJ$W(MJ@geTu&nOueCZi5HFb$nk(wZZ$|?xc-V? zrN3SwieE{yX$v&AR^hg0Qjm4BSr%+do#l41f^49>aeKSr(0x>uyB`QBxx;>ke<m%8 z2W`MU{F2LX&0vJt@PJy6)cLPo%!6J^pLBPEl5X-Z;pd^rkm)O2fRLYAt{>JuZc<YZ z>_yz0jUz2uIFGr}+@=`hZF9By5+_cIlMM#y2YADwJugj}YqdPLlIW_|RYB;Sj&9K< zFvWAdk)Ad-blwrspR9^Buj!xSf1Lc?4yWE&Yvx(lLv`HOyMxk${629IL0`ZM*%s?- z{C?pv%1`mUbo^D(K58$;t!S~s=Sxnz&Lda^Z$qG8nuKcrxph^|nvB-U?gkt%m^|_S zQM}g&Z<l=r1H9h^6%^R-V>!UV)AL*8x>0d^Q<VCGf2e=x9!mCS7TxUQe=Nf(-bsLm zn;Ig-z$0cTriYs2KW<$iq<l)w!$@I$U(7ZAXEuI8<d+kn1_#j^L(;8iIHTdZgx6~! zn=Mjpn!QuKr=+&QJ}<{_vwI=Pg8`W9%fYI;T9rvzOSM&6&n5RCS*ms#f;k^_mB{{( z=Z4O6Rjrsrv)i{p*@gYrf5g$GqSo?<wKx~{iJJ)F5wueJlLU_Y$r1<~{?vdgP~qFE zJD$(s?J8lh=?d&aB725msJ_^)=!3t($bhU^ST8{>h)?(9%h@<mJwKd>7^>WXv8Ny3 z;Jg4c8!1d!d)mP7?;+vcxYg}kEb3d{c_nh_02Dj5%``jMvt^(Wf4!N;%?e4kWg8|# z?RU)UVg*64P14279@P_E?e=@n>2~XeEp4jMB2GF<Vf518+UQYxSwmU@QjWjv#>-<f zP9sL{VQVxNv<>fv5PR&ZNuR0263omg!s?|_{Q;|yN>s+xLTy9PatfOT;kW`8>``nG zk~3`cz<@weqa<mzf2|`dgAa=UJ7Zyh=(c1D{H+J|>l->7@2$s&G|V99+Q6N_Y6nGT z#sjrd2#`9ak^ke6QtOYRcGpMUixeBmB5DR{Gej~BbnI!1HV18&lmmuB(2~$=2wJ|W zMzQTH^aTc^`tD11v|(tYPX(R(biPVA8A+V?QCpy&eM9y5e|t1mO}U0;URpt;TZqjq zdG9CgF@Xy@k_0z;Xdc|Av0bA};8bY@{1|#B_9u&pl$nIo-mN>#eWrK712x!To5w1q zTJ&^dRs0HE-tD8+fJnTqz?KW_Ci}ZY@bZ`67niPBk#PZleLJV4KB^r7?!x3Pq7IM< zVOtAZbqcs=f1Xw79V1%tCv-2LmOF-9?C`_+JBO#PAQl6z^IBZ)76oM<>ZZA$zpu`X z`AmI31=kLz)qA&?t4?rQ(itj4@sX0ClK<vDAZb|epF1#9c9b%Q<$h(41ANb9K*J#T z{>J-~!9B--4NaJe=M)1~($%G*cJoFOZM8tYuZgUoe+RmkIw@OU?9m1=ga^&<TcElF zy9<&cNQ`AXqrj6)C)b_<+<{Q{aT4v^aNFAZ%)n07E@lkDnDUAM8e@00t{Rxsm1R*I zA_*{B&qbd3w#xFQ0MVn$#<({JdSmB~cA19B0NIgrvyZTXq7492K&`*761{2}2gk#M z2S4YM`K5^8MSl#po}_>_QuoMI&Yxu4%Fai2V%yVROIqT*b!*~9khi0S)tH<<;$?e` zxrJ9O+zZvpM45a2^d%;pN85#9onKK|xEAJab+KmS97P3@y#3Jzj<oP%qL={8@e~OZ z5d^^D`|FjFAC$u0K(qKZD|W$W4;Mex=;gR52~*ZsPk$>V9CN3833B<GVD=d~Is^Wn zPedvNasGLTgqM@`-U(JtLdhwMCu+o$p(1zLVkA8ll{goZEIvZoc{%Fi&vo=^%q(z= z;QsDTIjq!+g%PnsRVdyJk1eV4BLhb_0>&la{XAlP_HC9Nb)FOp!>P)TaIN?%LUFeJ z6uNv98-EJ^!Cryk^>UFVX66~?4oH014wk(u+S%~J`LJ~S*}?s<kD3D`P3yrtFk~{u z+%XR?MVO0*P=;Q7S4c*vnG!^7jjP%eJ27cMHZ=4NzOXlptf}t6pT@tm(&w{H&a!Mr zXBcv|=O0d36fcXK(edbCBo0kEU$Zg!dr6u65r0CDuftR#nLOR$K!-cNa;I7DO<Y=3 z2_g&CP4ZzCi3dw^Z4nQnl*Vdj>4w&CHnFuaYk19w-dpl6JcWkER7v&1_oV#KHcrab zOvFb<G>Vd-b`lUcR8Z0IbD2N+O421+B3P6ZMeBCbN*1w5HQs(XvoUgCa*)Pny}${O z_kY>0IWl1~cJDt|Pl}qCs4QKpw6FL#w3SQFCbd8Q{sUMEAT#Yh`hDm4MwD0v@4lMV zQ3~Yo6gLXBR?4gT<vZMHjCVR=oF&u_ktd02ze#TNzL$7`IA=~_{2g+CD!qjuPrnGZ z$&*<_y^_0VU+(H7lZPP&6Vg4Xsq7=Q?tlEl`n{CM*E{kv*vb-)Ge4E`J-Yqz1dl9x zZ0Cl#vJ)F+t1<s8zOR|8u*8X5=rh9*^8psKkaykshMjvy2QHx=Jt7<X-9X&?iFEd~ zf=pmD_*m+DGI{WmEXhaw{Nbwo&<szT9?;TAsU>fK-M>|Wdw($(a>a$AdP?nWk$*h1 ztlNKdScw!k%Gf`T(M~^T#Ky<_*I%9!UUwi1&Yc=uTv!6a4bZuGmRdO3WpZyzaVylO zI(4BOPMkk(OqDPf2uMfO#h>dQf5T73Duu-^SE^V>x52gns(k2n$uWaViar!it^P-D z(De2{vb`_x`+Iuw%EFsFtL_Z*Lw}9kIwZ@>T5$7sjxFWF&&FgVGa)XOe$?^>kYUb@ zet<vQlSa_6O=+Xw6I}4B6r>C8QmT;Voj4S+!T*Bjc;*A9c41Ec;|&iePC7-rcuB6{ zy(9Xgh<X?@Bjo$o*a>W*6h&8c-{Z}_%w=f2m`m41#+Y$6)HF3+0!|zI$A4R~ONZH5 zi$|ZPcB@-^&`Gy!%S<HBuzPWgZ{;;hakq6|4Hht5D$I7fsui@%zc$bLV{nB>Y#g{@ z1C?;UFDOfCEp#`$O;;-{G;EDl$4Y!9l<8~$w8fp0bN(j(<p1Pgmf;-x>AaIPTUJaP z*9FpOyd7kI-j9hX-N3zH>wm-%H1ZY|f?+KU$=CF1X65$Z#+WVex(;jeQiw9h{8O3M zv5CS{e0c>}B$no?Zi&Mw&HLupzRbrB%w$J#m^Q=^D~3n-4p!Lw^cXqX)VX%h+Ea`h zt2R1wN9l<5?{Ij@pXYK*`fqfM4y-KEa4$4Swa0W<RrW{JYPb#W+JEU+iF0q6Q?~$0 zTrtl8<t1`vdT|3J-i{2!v@?AiZ@F2V??(UcTL1}ATDmXrD+cNL6fbvNAE{vR0S-l* zBY1!me3sp<L=m?p#8EV@05#(fvQ%gUu#%`Md)*fGo7&;Yxsk`hP0^NXeuQE#Dl}(M zLZ2f3f9*VKFwOA#Uw?+qv`{%V2@I(y8x30p=|uu?y7{y>$AvxQr@z!-MO6ym!CV4Z zaUd~nXLpk#R;rYqZB=)5Bru&SHP<%t4<PcTn_7hzZ)k*Cb5?XaqR32^vZGK)x7IET z^3k1uuS&{~6#QP`XNYj?%XYUKQ3nWXM6#fphLo<l_;1eElz+Dri=ShV;X!r!HcO@l ziioF5k@^bWl@A9lTu~_Kl;Qymt*bS*K{T1(BYYhmvv#n$m1Q)o9vDBz_TbF34bK>N z5S0Hpp@lyEir%bTV#>30(&y*H>gssc%-Qf!qNjxQ0{NE|dNqNRS0yFD!bw(i>kvhC z@>Mc@;TLsiL4PVcJ|GpFW(<d!@Q-kN2HW&!D&KA^N#O$Uz}S)m2#~#pn^IOlyEq+Z zl1);&9zII*P_RK$OQ9#c9%C*dkIjF1+7xWBEpE56q<5sN@viU?eMtSqb&KNUsC<?u zV{IAjLt`U`et(v`NS8|(MRQDR08P3Y_wk1dnuFgij(@6WzG4Cp4F!?D*wIHfQ^2jI z8XRj`h#3W8n=W5tjR0a^+8O)R0qgyH=r=`5X=&fW;QPcn{PRT9rANw{^d;r?L~hk2 z>g3L_t5h~hxAoQb)O^SmYV_uUv(q8|e@~~C^+$#~BTL%@XZAxgJ68$pg;INofX%hL zSTeC^9e>-6%C+(cJ2bl3pz2_t@4kt3cG-W6G8-)5DpN$;(+c?*9qCzy;hQrrQU$a> zhY42BxRm2kg6}Jtp1y}iqpFcm6lWXiR?Jm&LKJpyLU5(gs|tyih1c}lmW+3yqrk2E zdR!Y?&lVYpYhTnAq=lm^{gde>Q~WoO+yh-Oj(<wIVOtT_81o)4cNTk14q(P>^99GS zUKYRx$bK;T0|-6By!jV4YAB71f&t)!cSXCD%8?Zl-O`erI4`WEb;BmXj@GV8xHNpE z_5OU7&eJW=6t<D)Huv(;<UigYgSvll1Aa`=wq|}MgwF}GyieM|hhChj2pS;?FSck$ zxqnc@sD7?)Pshs3MN=ixA1YK0t3tdzknL2huRoSzdX^p7$=8a3cRn`<70>kJbii3X zi3cHrnd%*k!N!xLToz6D-V`tre=U`d>L1--kCjx-=V6Z@XSNP=4x0eV$$V9&mVo_t z!~O4Xn5e{NlG7o?9oJPuY)NSTOQEjVVt>xs6ygf>g@(f(Q!>OA=?1NKztltk?OH8( z)s-}k5O|Kt-}P3}Q69%vrU1AkiYf?x^US6pa^uhDCKZ^R3rQ4x%A#kn?9qS-Djb(W zeFF%~d6J#fx|F6wp?fIy(oNsF<~v^KzdzR%ph`HVcvVC$62cZzrKS3k%z`Q8$A6*6 zl6C3%Fdq-f4f~~jPP~C(qE4&S{;JT~hI7VUUuGX(KCKxEx#cC83c;t<7mc;6sd$|7 zza(wuFM$I}x1ln;pPL_~DCrn2*TdU4ku3?=a;VATKa|?L$i{k(kOV5Z773m>aWN<I zozPg1UzYlLYiZzsu@Q=8P2c0wJAYi!a$KIIys6<i|N7ol?G>j<eV1l)7)m73fQ~ZV z!IrDLQd3UJ{l!e5Go~01&iQX<MA5dzTaFe^)s~CgKQagpR*QnCBh;H`Nnj##WTkkg z6u3(j4Iz;eq=?<bwN%K{A;0qy)du-!g;F;LOzK<pgGs`WfT^#0(qjZsF@M#zr!qWs z_AoyMMXONkpn&tM&ZkHh@QZ6dkwgbIRwd{b=9Ywh<a05oVPpIS-@S(5dS*-=V7~FX z+NrJrGwS#hGGH#Kj1-eJbO){7Lo9dF>#Ba;&q(J$fUnpD;C|)o_`?L7Xt?u|WW?+4 z5vjeesc!odz$mIiNTM{W<9}mhI2P9!=J?9O+oH672K!*+;Sng>t?YP<1>0UK#{=tC z@i=3DHFuGSKGxj7q$L=)-XV%dX@5kuQMs*KE4*4q`MKDr6dWXxe?2^jN8<wwRit2d zfdJ?{@{?yUzW$S`SKZOvi75+CG@W~#wQ<w{3sA}921m@&@Vntmy?^II$W{ddb2Mee zW_-ERc(C3TVq!?e=swQ5;hb)-EX?*AxZ}Eh%p06n85AQ-%3}dDBq#Qz|3ek|Nlbbn zC`trC&Ar;V=wDF<T}loPtA{W}Ue(2gOOVAeWNE!}?byn!s0yP9z*!Gp%56(nG<dNU znx428kD3s69k<X$`hO&F(WV%|I_hHO=m-S7+@m$!p<lDIG~TFuCN#30kyQugyZa^G z9EqVciirJ3G3jPwX2?EHriWk{oFYJEvL(r8bUuJ#Qf<IerJ(;L8}%F)x)NF?@H`y^ zYn@v#d8MiH*7128D(=e}ZULOmQ8X$I<^{sdl8PQzlV5A8s(+vi3;d`lfe~a1U#6>* z+D^ppL0nAWj$)B^65jgkwV0$}?PwRSQ8&l#C;*v{XrzXofcLn&a{dxU%o7TqJ7@2w zS+bX}76V%Yx~!P2oW`F#ag!mFt`LyzE*?_+5Zh)0L%C2kS~5{3Rzon+aIM&1S)9_R zq<ecaZEB<5*?<3mN5b?8@Zcw974plYVFxv@fBRXa^#cufc?%m%>8Da)N)tbK#wdAF z^3clzD%BNh<W$y5m0CTP*K|facOM;~^FrFuszDzl%fM08JiH!q0xfo`YS4gMike)M zgeu!mBh5r2^9lB%IvE%;9+h}hQsj^5KPS1Ee4&ePHh)Y4HiUjL=wbJ~SrnnTi`Z)o zL3Gb`u(oH0Xx{tq=XMB5_;brE!Ui1Bj-*1~apchuZI>h!yWKrt+R&|RI4pq$n<&}2 zG-NqhyV52n4hg4QIO{CODks;Q%4K_0MDhJY@w@_4ZGUC)g3Ua?b?(BxL`}aglEkT8 zUXzfN6@S7a>o(ib{X{66#0Sgk%WPAC&Q7*tj5mJpdFEY7uTPM8IH9zp=%^D@^gNRL zEWe{<8>X3=4p7*5LK5IRH-8m7TUHH^GYPQv6o?h+_oeEzE$Y7+!QCSH!3YLzJx=Zs zXdxV2^o;JN<_cjl^+>s#m(R;?t7#nG&-BgPM}Ii2pE;)emFXwQqUFkK0BJkL)Q6@+ zzMm`pn6elVWo*#Z$?iFODxeZ^pTnBrj_|~a6yd)ys6~j{e#&r$p2af}-|Vl&g%i(r z99qL%0g&^)cp-6~--0Y_p8xG6Zjq>D8=6k5wrD2B*~ER}?clJiT~N`gDwz6Unx}d! z{C`9Vyy6*F&>4N4sfQ;NQY9WI<J-O&WF7UUjvvPVAdV#I!X5!h6sWVyUlRpz+2c}Y z-Y*nmZPSPXA_T)Dg~dkE9LRfnEhB3^8h;RFXQgL-sUxM*r&@WakWrG?Ntz;>b%VXD ziq_Me{OWS<r|zB>vK^)3pk_X4keY};D1SDVU~pM}Oo83+aqw=Wjvcx%+2`mOn>{!s z|6z8eP~kls>oMR{cjtOk0DIhKetH22Z!W1_&nz)v0)|859Ze^kNaq;wasr=WVUD75 zr!RveATd;+R64vz`qTNCfFl4EV-BT1$dt>E5@l?^nnS&K=)4Ce5Lhbd>G-bX0)Kw} znM1Xa3`BDOy#tWr?4g6hK=ea?E-<3vfc%`56cN_CWYx{TJ~o<su31q7z8>;9)JiKk zLHw!o-KbWFm~V>L;1wIk)|F=T_%;R<{9=3JCJ*R%xp8D0)hiBY?wji0p{&AvCdx0J zcO8(jCO+6p>th6O_j5T_0L2B(Qhy)~c4+`~EcD1;E<DjYhTS)b(<Ldu#YyO3VCV&l z&H6<&CZ7ocB+hAXSzprC6&}GILWUSUa&xH|;S!f2q){|Kq8ly?{-_>l63Z@(R?L;g z{KQYR6xBG2Yf7UuNBV5q9H1hF0xNhx9l!3OymQo{Wbh-}#dTAoxdF4ErGNO84`pZ9 zTKiB*@+YQ4e9yYfE6IeFIM=N>Wuc-HaZI~Mn1ubQLd+)<dY-L0RPftb<p@XFxvcl> zn)Z~{FATXyy5{hOE#aCTVIC`;bVSk{xd~9$_NGTAA{tEgd;7v^SWHrBNQHb6!fj7n zn{8l{7T!>c@A?5X_=Hd+d4CX3UTTb4`=ylqgY$ubFPe&a2EYmo&p~NqkG5V>IndDF zCgKkXLc&(?U&(sc|Jx^H62xaz#4f~WB~DC&fyZn;pSIAkK}jBCMyL0Y%H)E{*j<Vu z`(AHsB_c;O#$4NIal*K)Jh<sIiNFv`Pq#eR{X!e1$*u{8?Q3fRDu1N;I93*QU?}~L zTzfL#N4Y4*)WTJ@@~#B(VnK`5zj;@y$r__QSQhTR-`+*6AA;aFJU#flcxiCinr7+0 znRUSrCri02eS@!46<Y-5&y*^rln3sk5`xz@9-Xd~{T?F&hJj0fyC=<b@`XPq5zygP zv#TmwrlgPkiPcU@`+v|mje_t6tGP_fOF50zkM`c!rV$3MCrfALS<{C6t7GpIxws=- z7qF!e6w;%L24j0<OEICWME|S>Yh?-jgKTZ&@_S0y%yPco!fX1;q<ou#1Q%#nii}+W zl4EpsB)9?rck~S-dO~R2qc9A@rNgah^KatmG=^bzl+waV5PxnmR~_Mpt^71IsE&nl zLorQAg`dL)OVlI}8A){v()eVfc?J)rBvY0L=T*B<1y{%4Xr7|+N6#QHW1gbqa3Ja^ z32!~9L7a{5LPo?=@q=gcQt*IE&QHjdjZc3^cCN(LiGI41OL@1AWg<-th@6YKL_GXz zW?L?o%q#{%qks7Q4g9IT%O7-D@m<hg$xz{-O%?`cY6sXNaSxJ>Sq#T};%H%h78Wo0 ztet%C%$P0|fX85h6MwqNM2&cXH69?!RJqZNw`$7TAiU*t8#$L~|H}+aEnGjCp+JyN z+4D=~AaRF-|HOe?@_kJ$CZ^_)8@Ur^&CmtIbw?a*UVjR4ZEyh67=l1}xShGB%oHSe znA_tRvq+p^YCM4mmi-XwhO^`G!J5XHFac$<>MwG6G>H!&xcZj9_foz%hh!UqzIPL_ zqCyPm%2B0_R}DA&wiV#6v+UuTy2wnw@t~(iTe&tr_?0H<XN8Cw8N-Le97ejaR_r_- ztH<6z2!EpbJh?q8L$}k(Qa7Wpn8l_r_6vj3Mp+2J-Lg)^m8mwX(_UC_%6MykB$(=Q zbyveoDDP-$@l2D5lW4#cB1VbzT|mf8uiomDJlP%;&C4CFHzc#t!Uku_#CPEE$TxJf zY;D#GSv~af9MwA}BnirZ+JuKp@yhv5Xlxo}lYcz-66|DNOOsn*|9yB+NS5KzU%GqS zdFJo&$w@>BvyKjgCB8{gp^;D4)`nsR%KSSE8t)Ci&(r%QWNJCFW1`?m`Do)Zph&=3 zrHp21r*fd-u%XEU*x~>pSaokwxe?amZM|8B^Ia&pL729`lvFH|DU$)jvKxo*XCl_k zh=0zoiy1Ck_!)jL1UL$1;aqT=s{3^D>#1_|ZbDp)xZ0O#D3XQ6I;5W$yo;xd0FKGF zcR*)0S<{CO2yI{KG84Q8r8o)R(qwk9Rrp>r172Y@k*FC;w7PuMf#<cl>{Gs95@5=X z=)Jo9D}#adlSFC3?Bvoh-qsj<LYIRYx_?r#g8wX*g#WRfx=_w|d4R|9d`FKM9ujf3 zhOTPz{z~NI$43;{!eZt<ljjS5?xz<9?5ODDY_#vJ1D^btJ(ASv;~?;zL&o7fWM1_= z+P1puoc@Ppn5K3+S^GSla<w3kZpDN5LH_m1F$<oKJ*7KP_1rji;x@^`LusUhbAOY^ z6`DwW8Mw76q;bKYz_y!VxSZM?nrOsD<4AswZ{EM!4(z$E668@3Zb%00d-8G@Nuzw+ z&|g#}g@P7wvMC(l4UVaiuVIG1z7wxMF#2CwiOePq9Le0f)!Y{NAC84Qw_5n%l+^99 zcTW~AdwpGpL(*grTd9?FjQe$oQ-6qf9(Nnnn{MupFBj+4Ck_#CGVXN=JnzN_h+N*j zbNQdfWW9WG^C92E*34VmJgV?+-2aBpFuJ)^$pR>)1OG^=@X}ay)Wb;3kth?e){!IA zcEpfnU(9fLzE?irP}gv~!$X7hBpj}UZPG?(s!$w!0Ta99Igs}|lj=z*gMZ$ciUYvn zgt@dehL0)jQ^?RK200Hfko`BcuM`?IGt)mcJ6dXqYP44$AR-Y6>)vkBen(sTO%UDy zNK8Bq{Kwm&`=f{+FD_#q|2#FK%ttEg_g4}0hvyDY!bwm;PYAZ*ax}f8L45EiBf~2z ztZA`e3@-J!n5gW!P#QAMm4B`B-m}D1Rx<jOb|Fa-Kap{|74K)JhKlRI&Od2l#yDoG z75H~dW;@}h&Oa1(0Dd^}sC`wR@lDVOVlj9y0s$dTF#0HuQxv#PlC%Ltj9AGf8e~pw z6Rkur63WKm0Y6EK$A9D0F`XIgg|n|{8<hd%DB3BWvy1|@@|irbynpR`gr&!nJo<{w zM!W|8u7>gcq3H1fn9_lTy~Mt~GpB=%LUcWATRG8_nre&A>pi;8@21P7C)8BPJ)e6* z-IL?)^xt@RY_7yiX674<I7>l%K6dlVk;%}q9dka(a{DtPBo^6*i?m-+9Fssk5deAY zRh^xpVZ%otJ?Se-0DrQP{_fOYKk%@a#6OCfNq!>FlsIAXdD21-9FOp9E^%{Phw|G( z%L{2V04hW(2NVQv4DElPvd7$bYJinfd{!5~3%h6#)b>k1H*WnkkF{cI>`6+DyrB($ zBa9p#!#ws;@Hz3WsnS%B4VlfML&;|9O2D2X&JFNJmYakx=6`HBq^Ne5gI{~QFlsT~ zzdT#h1lpq&biO@Xu%37@v(?7;!0uOeZ%lF%O!9jkcyHBsMuQ(#NpZkzb>gO6;XqKH za(!gB=|*g8lM+kq2s0(#27!MQz5Dp1UQ}{ZzvAJBT{XrUqww<0+B$yxzVo6|QKWt8 z8mAo4&G187NPlb$;#QHTkCPoChhQ7xv%CUBBV)AX=t3E7c&ih+;5lO-h_?Qt#em$Q z3BGP`<U^jt3{?py6a9c~K#KZ;DBk|@dBx@BJ|^jH<ZfTDUC{_M(K0GqaPu|2)1t!h z<!CqFfwls0)6}tx)sjivXYMlpkFqKe)^RAK89jfRJAZ^t-4DP3EHAEfV}{tN<SC~( zN%041ucfvbP^&4hc`qa=q`cV%7=NX&u9Pce(Wp}5g=LRRYlLw3w84?eURX5-U>Sq= zb+I=~KEh73A_>6(>gq+CQ2S7Cd+42s@uXhqj+(8VgNqaEel{i>rsQJbCX9VbDcy-2 zBSZ~N%YQp%6L$7i2F93*#M&Y!9hk*wNd%SS$y#x|+ruq1vvne;A?jozvufa9fitYZ zDSb-B{)(5GWulhdvzf}VKc~T+<KxU-Wgd-ew>$~;!3faf1F)to10z;Q_oLX>SJcU5 zKof^ytUVO>MR7|cDAQv=*yd@YiD{v^8ILkW!haFqo9cY?PwK4cTk_M2pdh9^p&0VG z&8kT2t)DCANy3j1G3aAS%!T-{do^a~4%Ol}34-=82q6#eoo2t}*SAqQ?T|z8sk2sl zZ~^lFJy0TS6sWU#Lk}&31$sTkt2y6@t_or$Mb!m0Y<Y73=~rdOlu27`*nWRCr3SHu zBY)VhqN8Yxww-U<TREhGz)dyFD2oW2wwW_84+1^%s=6WzXOvB5H0!1rd5?|{{siD; zQ!zGxDje#84&bb(FKoCYIO%f6Pd2UMB}giLkLJIm-+44MD;2g=ox={cRE_aOP|1$! z?s20^&9cOiP$fEtP;7pc#wgsBHM6Mb)PEriU!0el-l{5?T3lnxqy0DTsSoquA0Xet zf*tuYjY;ZxE%qDxW_`HiW|M9hp;eoewZNH=5j-AVHgw$Og7?McDGe|s&86Sek8{}T z;xc<hv;XDiqOudfgNY}>fz@~r_^mUw04h9Y>xorN^h}JfUh31wf}4vGc<L|m?tkQQ z^zrs<)QOTA2BYL<a|PhS-U=`h6-0|rS?CYymFR|~m*mURY2V!r7={?wOpGLMZfU1% zzbCjqptC3-F>sC<vdOs@(U4PC%cfZq^pi2u?kckh9eq9_;soLLB>XUcGYkzi|GnXt z$0LazpCeRXcJ|lC@gJ0_*Z=;OWPfG+Xd{gj8HNQJ`?gBONYgDXU(<zk4MnASi2j?; zzIW`}t7MFlKU|NGLB}T0GT<b(we1L(0ruN(hi@OV$M@~T%>3@lniE1qe}bInqWL}} z-2h>-_Q*-+%a4ZX0xF$5qSLUu5;o6Hl>Cv8surWxP!4OY{6z2<9^WndkAL;apc;HR zeFDsRCKXNQFRNcDrMD=X)>9WTLC&3K2db_Zrg=9Fg&AAQbPIo!0In%p`PTo9rU#+C za$dgZ!A$D4+lyHgZ~ppbu@$;}Z4||A<o_!AV*GgdV0Ru^UHbBMQu}HI9o+|=^f5~* z{U}bzv9H`+A<0}^uD`{H+JB+hksajVsBR|fxy}Y>U1l+}B+KGCp6u0e0raw%K36th z4T<4_H{dmfd-L2<EBxi#VNgXVpoaP?UHSy`ukBinWiTf11>T`zeCBAL<~wF(4oAZK zP@ul0L(U!kLCmE-@?=0LOh$Z*>|&xeMX>diV+vtNA+&^WE&!#Em46y?R>OvT`X;)_ zqbMK?Hp4=1NP8=9%PmbjgIeF|K;#Jn6<m~~!5C*9Q;(bt74Neupn!ss-w}f)P^>@a ze+rxtx#T$)+IHS_MNj0SK}wxleOO+)zC0TXM;<w^*xH{o0t);5!u4EbyXth#;bTfi zMt<@4l?Q@1*Od}-uYa{B7JUx``B+qY;PjE*GZ~@!ci-Jrbr2nBOo1TZ8JY-)F$G{N zS|PWei;TLo`#18eO_AykTiC;9AAcpL&fTJ%O0j-+h_;D@UNACZ^LFE<E;&(a$_V*h z)NmH38e|<l*kMMoe*O*$1s@FfQ=wgk<qKCt+SRX&_*TW7FMmg)KY|_?j++M#6(R&F zle4nh!QiHO{Q=LZ0toPm8Q&iKGc3e=ZX`SX_!slI1>NF46z~QSdH@{6-tA8EyZG6K zVcB`<xwE<S&BExR=GEw4i`8<O_yQPAKe2^?(Q;ls$oC^Yf|!|~4?7rLqD(;x@@jDZ z0b@o}R*G^5j(^^8?I~OVC4VDjDd)K#f;HIDc(HY4u=zN`SYvjie|`Q9N=V%gp<OZD zHOy?g<T|eZM-LKXvh#LE8~M}J_XA4dF=h+hXjUYt-O)G$dRrq4=VC7cbSiu;#U_!j z;`zD~)06X|=D|<@+%ih&@qeJDMOt~GOim7xG4<4FD}T0867)>qw4=bJ_h{#Gq3m6z z94$rDVhB+J0R6dD4?G<IZQlK+2P1uwlQz$KY85^OI`3O-FMYE#0z^YyKemr7UZTF$ zUYXfB>dZAPlRg(Q0IZxJ!}G6qw0}$y^FhBDJ2N`8c&f@{bT{&~dF`bNn4mt5J<?yU zajsrwCV!>-5Oi-gY(@KzZ(O>k|E@Owx`hDsVm)N#=iH}^lbIHSIvB}ARbm+bdMJK_ z%3}1w8gJ#U6R$VoxLXZY7E)8ZgMC+YI8BG7aO~e*%Fdb}c)wZNMqO;G91zfoxmI4; zCXw7m0&{@&M7kMF{iU5WV(u#Qz_q`p;fe4uSbsX00z7Tevx~*4t?+N22inx;3*+p? z9A?;3>XIu7NnvB(`;#(KQZRbn_*l5@YIqlCvBnDQzwA3HoLVW@bUE<<qP0Kj$9;fO zkn6cXe=?ndY|dx5<92m;aqTf5Ic#VHo^3sLr_E;kbjM;#nojIKlC0DQ>cv*|QDN@| zs((U;D8~<O7t~ch^)WIw?RQl!)Jyz)GRT(1B)dK9@J%^r^7B5{xANq4%Q?Di@0kvE zZ-7^tGl<9=G)=_J{eG<tu`u}cnS2Z<XFriBwE*3iQaI7@*}1>2s3o3Qq{Are_Gnp1 zD~S@4I);bwkC?7vTO_8KriJ=owW4@Ph<|o~J6}bFJyPbL=uP@=scjDz5u#f{kf1%y z2zyVag`MDGTnOBRfflTmVhi7&?Zav4Znnq#>NuGYNEn>jD`OR>27|5tK=&Y4cz-Bf zy8wN1s}wUjtD|XGqscFBt%cC}Z1wA5JTK24%`SP+nimj)d!+%@R}LVq|MEtzHh;AO zX3W_i*instFIbNmoc!(ZG_{24{F|p=$+BDjTHpFwJw|+GD~uuLdwS*Dq4+OKzj5;R zLy7(F&pBUxtz3>LyFzL+4_?vMO%kWOhiFXTt@{JN+a+!F$lvRWo*Qjr@URsb6f8oR zdy5=@_|O}^rEWQrx0QGJ^B_A8e}5M$2I{1*3@aP4V)5Ut{k#JIq3l@+o*Eo%<zH*w zI4!fJTyBl~Q>5(8r2RK^{{>7aiwJ*h;gKYiY+r>dZ~dj6nZ+^+=^H-}Qh^lhd(Xp~ z3B5y8-4`IMR7L_->8|}{5<t)U8p`i^JJEeMC1bwid_<IA&YRIrJKj;&Ab*YjU;b62 z@@$QiOW;wdKKRZHV8mnpTgyo``Hiv+xc94+X?KvdEYf6w-StSc@c`cPwl|<FS9qlv zTGMosx!p2Sw7g<>=Gn(V*s=+oC6@PVSl)E=FrwsdrTK3WfC5H8h^;Y!rX-CANytz& z&EdiOP}hMBVmU1d&0PE(Hh)M~<jgOh?-Rj;5mqc`1=;ou$Thw<U->fte<2H0P~Kg2 zP?OdLdSQt?J2~(MxZdKmy&2H{A<Qw@EBmV|r@^tYv<!c-p!&ngL0ND~j)Y}@vFu=d zJQ$!$yS8$Pt*sQ0?a3_{i11AYNjpl#@(g0&(H&=M^YxmQ%ks)t>VL361%fHVtw<$5 zBncn;`#{QXjOcF3J?nHf%k7AW(UE^}?D){*5epAB9Mxku$iqJa;|@}`$0K9dsq&NK zCN|Qx9Zbgh!2W;iy>oYE+qW)Uv2EK%#b(8-7%NW2c2-bvMHQPB+qP}nww<?X@AKYs z@BZy`Kf!Hl{<GSeV}JHBp3X7bSp7l4O&9l(Yx~p>a_h>A4~d*}x-Ac5>tV^YFK+Zl zX}X|qJ5XD#2G^wR2j}jnD=BK-y%r8_Z_1C}ajunv6%PjqTMY~4eU=w-S#|uCJUJ=T zBrV6)hH_-(W=fVmF9-5*aS+O!J@$KicD4f7l9XM?HeE8>7k|bj?)5T+F5j-j(s&+W z?d`#bL<)bI!u-4`!JA2E+d@r(j{{ppz%37!$9Kk*VH?||W&3Inv|7ci(8M)sGb<nl z+6@-FXYBsL3}ZiNld3&PGR=Us4mC!#3J)|CTsIVrF~jBn%YU5_Jr<1+rOHyk`E+x| z8L%Ghh}7J`4S#kfoL1=@sL0{_*XdJVEoiql!ZeOu3wY_6#IdXUNGL^{wZL96G68QR zHH`u)t}cPsTKoe*JFd^mf_$mg0`~kB64B@hzef0|>BFX@TIolzHs@ycfUEw-yN5~e zgVOO$g!;i&cx+8Ct}c^x6B>iOJ59nu_)|+^v7Th$d4GI$a?0McMbw(cfMK|34h2qN zopuYiOt2gBpgc~9Oo8OPpHx!V<<HZlE3#Ka6LqSsF1Zp`R7MZV93?>d>Me{py(~G- z`gVMJ@L@U|$WCWbh~R~7TAnnT89oPEM%hN!1>#0U>@@qFO+|nVAoAO+Iu-ZS((pUU z34`u}dw<9}HyZOEL2x76d#5Jt-)&z8$+8w847nw8W&5g+-D3h_<ZBraWSBqSpd07s z8yx4ddv!FbRr3`Q$c$KNwAK>dv=@uWm7z(E;v}Z$S<#vn)W{S9DXm0c5AVBOT3_tB zGraPV=@vM$%NUU3DV_C{pVNUJh&FAE3a8J<^M8U_xxiR-7O3Y}o(p-|L9X{sv*l3! zKgY_t_Q8~cvxeq<m+OZKwI&UUz_(?u9^a}Sgx%D8^4XH~@p-_`8314c)_bcZegFWE zMr0Gn{t?#Q9KF>+gg<4MdJR;2{&r;q4jtIh_Dr<fSn6c*_E#1YVWgfc8avyJ3Rqx{ z9DmG+)I%ldgGFfhP0hvC(Pe_QUqv11vZADy!1FFXxy~JrlY~mIGYQ0B?;zGtzOWW_ zWz}RAnpnFQ`19;vFID|A|7>cS87Rz6GdLU`{PP$qbnNUt`&$&o21Bt)wEEm)eF?%{ zg!~}_kFhEK5j@&7+QUn^h?=^cOS~%2<$p~zq*o{DWXA8Ue7lBXU{0Hw6HmVgCT&g6 z?9kEU%bQVeAG83q7k(-p)<_H&vpIY@;SDr6m03a0*94*CQmH>3;5ND%B_iay1Ru=% zK6_s!4k+1{sd=So;BFrOI`15mb<ZA>zI3pDMva7sq9)-nHGx$Uf>~%E+Gp|_1Aq6H zQwD?YSn-Se&_x8Wad)~clU)@z=wq`67JOl>PAgdtzM<Ot(6dugBNLJBQ4o4HVPdvc zb+KJpS!@H+c%9w4rXJ+QIrR)o9_-$rOI7~`lMamAo8JB_>*fZ;LRNRPO2R20rnt*R z9Cr>p3G7ML$7u|OLg;ubRldP=BY%4Mh=PPi3Jhi#2xbkTJ?8xnHe$cBAB)b8EM{6V zB~TG^^6Z~m6m={_-Qx)`PU|WfMOEn(GMo%43Ez9ZVc&Yt)A}F*Fgg~UX=Cn*QdqE- zV0y^!^5p+&o&#L0F8T%Xp?i<i$Tu#aHGQNHjn8?_p2o8OT|Y%4H)7{Ra(`?2T3oC@ zvR>FZH2F)jIir(EaW*Wcttbr7zD}XYSZz^^$^BX-zH1q5JQA1LspALqgN=I!Zn2#M zijq_&4|XW0H+gw2HVVWu_N;at7=f+slq4Mw0li-%8{v2Nep`QQ`M-M?qfjW$D1G;_ zY80zSd@>){eAxMb2_qqFd4JHf-&w;m86gJ&qBXt`8#Err2pje;bXU7x$qmT>U5|eS zRuX?##ESeKq986_8X=!4?g!%8Y>)mrChlALBY*SpIxHHr>lWG(50eRhzybp&n2=!K z@0TrCIyVYT;)i-&B$Hob_6IY$4(SJy#jPgOElx6329ceYvo0#N6MqZr9kdvJzwRS= z8T6oE<<I35yQh(2XD=8Pc6jXIN4WPsgL?9Az8z@?uPKv1Q8FKTmmJaP$Xa1=zNyj# zBV*~-Q%%1O@mCPl)(BFhgavJ$_Xy-lm@gF}VN{6^=c7<D8PdSQqc_!$NT$q<g^@ug zS~Y0Ju?k}P^nNvRbbkw9>oP4OF-wbS0zZ^wBfGuQl}$6Gf%HD~7E7!tqZhQjh#h(V zirP=@9MY-LhDhl&ATa8qek88papwl7JHY}*(4GjvW(zsYk$AeI$=3?vuYn)>OSbgg z(iT;r%*0bT;mnb|80&48n-_&hJt0$^0pl^G^FY>HB=k(xGk^G{x^IKHL$Fs!K?R3Y zbHzn2ec%NytK3M%@9S#3lG9Otx*nrmvQZ6m=0;yubwGG(!x<ykICExay-V&EjER^! zP%`qkeoCk?H$3v+VtQw^B*i^^J*S&tiO#ccZgpv`J`=DqkNbG=w7Yg&<+;*Sa}ysp z_994uECwOZ&3^&?^X~=n*>|a>L3u)7>$&T8F@Jo<;uM52?7$M!HAbGcuP2YkB02?F zgfR+6NJAw@Bu#LM`mSgdZU{kLvEQu;yIkl=LkMF`t~evoA|7Ql7#QMt+j*#p*isB| zN6yc>^Db9Xd68y3k->j2Y&-tLpTx`wxr}U8%&r^ufq!F06)O>OqJ0lV?Tcqm>Oi`G zBiy3HC2mtgon#U_C8(GX4QO{wP}JPVutH`iq$fr(kIM+|@KK!saP0xR7dHQZ9HGBr zVzkRfjjW_~no)+)84>v~(WZaEc2vy86kjVH!OdXAn%a7U6i*?A0_s8D8^OL`>>gfv z$fnv)B7gLggExfv7ZOz_I&Lg;`?tyxwRZ%Z-O3bO4r+d+g=)|T%KDs~P3MJS%F<6X z0^}EZART2=bQY3269HkF!S;_z%kKI)!Nx-i;r5GFUyxxlEKj#I%&I%N?!UUh`uJ^3 z(Yn7^%_^C_`rMC$2@I3^>`a()wo#FjBa5Jdf`6bAXZus@XNSDqj+eWxFY)tFoIH0N zAEkS^ptrbsxbTZBDI)(|7ZyPvCl?ZtqOgu#JN@9iNU)OmLJTvspX|l)l2Ejf>^@S{ zkqi^loIyutW_p)qXbEhQy5<w1M}0F8usbgCxl0pqm6d-BP7B^7;#=N%b{UAvsQUQ{ z=6?|sgu;aygr3)i$0gqOXLcAxfhv7Xo)UN^6UzAp6c}017GLsDerJ8^gmNx#t*iU{ z&KfiF&7xpQVhzdzL7g@v1=|*kyt{%9L&r)I{D+8?;KQu;HUgHB3kl!tM7*B~F7bB1 zf=(%hnSZozy_ti|qf!^_&=_7h$5MJhaDNe;ai<<rED2iHJ45e=lJQgRm=PB&k6g_; z8hpvLlGAN-yGK8`jTtpiwk~1+5LPi+r&<~2<u59??wf%K=oECs9DRv*HZxy=U6TPG zfp+JW1JEtUmH4B4%WUx=Bcq?7j0~0^bSLOUu@1QO7p=enG&gy7%t|d`EG*op#D9K- zLY=g}pUpuR>vu+!jEcF=bPf{Aej2zVVB_)=(hQM2ZkfDQnR{YH5*>7ZI}3d2?x?n3 zIu(0-I;w0iM9cViXPiyp*_Rw@k>HC0+0w~wY;&H*dsTV0kYu1^(`tdxkRH`PrCx`x zD`iZl^2>%Hv;PGR@aV|kZ`VSIWq%ECL{8XV`$o%jOHUmoB<D-u(T01x2nL8eyDMlT z*AA9CLX*s5arJwH?^uHdH`d)oV;X4o)qj-Pu%TV1ym=vTp`&oc0O_c7JHZF6dB3-- z9gnOxyqFEYdixuDcT{L!t+psYgg0qv-v)N8U)Vs=^TRx%g4~_&UtoolE`QP(Hq}8R z$Q1~FW$Cb>yN|Q&|5=DH1}<qh4gqtRh#Zb#`Dmo2y*yq^WAFBKgm0Z>WE6{Xa|7u0 z^dw4fJ&sJ4Jli*jB4#s<xsN;_*ow3+iqYE(-d5)=(XRP~21C!2iv&4fW*|9a%AUfT zUsKOUw>JOM7*}I&azB=$xPKTM#8VIcl?pWDBV?&Ejv(M^@n{ThQW=;3t`Q(cDq`z_ z;BD{kVS$ozW9=!W7Q2b3Ox08C$U@w4KJ^(5I?aLiyQ#n?n;OaT<-Ss4R*^AM$}nsS z+%UtV_=rUzXB#3=R(Ybp0IPEwYKVCdAEh(bmNVJIyNRa~-0$EWReyNq;x@T`&F>pF z2Fy=iBxJ>&%^<O;zT#~Ko?Q05{vHjFLtT{l@66N==+i2q_I-VCm8PQHR6VwVmorh} zy>sg1q4oi5%O?kmB=b%Ld!|UO2nV+>ew1EE++9FPgS#R(YVULr=VgfW%Pi>dLr%;W z8P{L^ZrkXF*KNdDQGczFGf-i9aj#3<PhvDC&>;qk?!E%h``_yXlyYYn0!Yl+MS8bK zm`LB9ejN*J&B;`Abquz%9;BASYgA@9w!<K$lVEpYeIS7B`0RQz4NNz@)tH828~QbU z`=$2L_Lk{$tXXY%0(S`)aZP}(wUL0K_bc{yNj1i^8}HaIjDI)p(Psob*sk$hPY*T$ z7y=fI)%*@Gpp4+8V9&%XPRtx5#}Nd3F6yN{YB<WtgvtJkl^Qy<J&%StpyJ>=UR~qi z15puOmJ^W@AHDjs$;DR&>pPF%Hg>Al>@OyawoSK&ZvhsLZAy<0`4iS=py(ZG=4st7 zKYf_{k@)ab`+ud;)iKjW;M-Bim4n*fjHj7?PI1bl;+0XA9xpOpbBut9Iv~P>QFX>O zpnfa0R!48&<+i(?K=xYNFeW@A2GAzRXgV1VpJxJTvgqF<51P_W<aoVVic$v6zu=sB zb|)5p@!XInfC7FfF`vE3)4zLMtP=RC_(W!L`yhAKn17AWEqZL!hOT6@Xl>NvFH!X@ zA=MK022UKn<dW_7tLLqah(Ipm19T8Zf=g1x$v{w%L19Q}4)J1w7zX-48~s;9`<1=a zUrBKLa@-0_nYo?puo+WhLyYne1_dWQH!#rkC|urdm^2&JO55N)(xIbdE)CC_Dk~gR zpr#rm*ng@n@)`{eiNqf*z7sLD^Td@^i0AjHK4^PbWYb>=PBB#5G&n4KxbX|DJiAoj zL2^^uP?r<Fv4KBVB5$uxrWB7TessMt6RaP54;MVd#|vFzeE1qnl5e1kXce{vArR$w zZ}l*w-L3Ul%ry`bVR5HgDy+epxZe(=&bIjwjeknIa%LVwXj`1O|0LoAdFxEmGXXKy zdBY5sX&$H%Q|GAhEe%5{3{CUXO{SkA{X_R*BdibD!)%Rv89}V$Q-%nGlkZ1@`ar}{ zGOBe@Q~SLAq<&NNP3l66i;jf^8i}77y@##43_pC}>U>=?TDx9n<zmpMCdMZSPQ`oQ z^M6+)4=U=819H~mo{+r<o(07hlQXB%W^miG)UfO2>6x&~-~a*J!@i^xM$sUTA~x?E z_HfIMLyWwI0W;#qVxKOiVn79I`)RBoXZ)$e*-MAeOz5_r(uOeBt3(jD-WuKTaoNv1 zxcA7QF(NnKJ;wp~Hwlu%;ekyoLMoG*AAi0A>R)Ujrc2YEn2BBq>#mK0^6FjbHHp{{ z6j(=<O0}%_9)f}f{&2%|F2Kai?vB9vyZ)-)(g?=-{MitlQNB$yNHlVO`<#JPLnl+{ zb?Y5IeE7#K{D}g4oPog>o`(118lqV#X{*a15etg}Vqk@Q_DO2A^6PPchjpA(b${X> zL+^DPz**#UjcnLkX{ga^_wGx`A=AW!oP*y<U=M}W&95Yry2fWFllT}T&t}OzCp$)` zcM|BIq2pjw8<Slu53Psv2zB2y-m4YhGJ-&iK8GO#t)wfeKRK93+_&pif;a=-bt!u$ zT)EHac$+(+!R6cVKz7Qn<4MyUxPR%LRhb!qC<v+R+<RQjo<gF-#yHc@o=+gZd@jyb z*4ko{1b^$BkwVdcj}~;1pG$d#`{8N#l3pj_-Vvp%y3Rp0{abG<C){OUcMKzgnHd!( z8i6mAPqJTHwDj%tmY67aJL7W$J?y1R0XYFN9Kx&dqaX{Pso@0>=?uNPoqvd?uR^}i zs0Bl#>Gj9!USw2diLo|Su7ha~JOcc{sDU~$ulZ~8kPxT5DnbMsBM=kQNR@Nk<fR&u zd0QDg&6D%B`}xvDD!V3>RL(@a5OB#X3*}{Y1y<)^w)uoKDE4tsUlDx^y3zmHHtdia z3zI12fQk4Z7&>n5ZFbe=-hX8cuDE)SbQxm8=*8gm8YSrmN9}1taQamkE_F%mJ8sbJ z5I_CFDB~YO8z%uj4;2KxMQ_3D5)M}_Luv39u02HK!8HBh&+P05Blps+@A4!r=f_PK z2o`(&VP+IL0Ff+84^>XJ&D5L^{h_W8C*0qj`fZha+Z-MmPkO6lhJX6SrWd`VIm-ra z<lf+DohjPY$_y)ao?9vaGQdFk8Q$8##|P-EV^!M2$4ga5h+Sg+7?RhHi*T|o-t*j` zf7C@6`h=7o+DRkm2iPFFHl**#n$6vGA_N~b#4&z7H%WqBa8Em-f%=US>1Ju>F!*Mo z1{Jp%uthDs&2~XedVk}+;`Yo{-~6y~E>s8Y<dy&PLhOg8U{JdP?TZUtqnvw*zK#IW z{&vR8PCUTar)v2v**WBw+PofnZe4uy9?c)}pG%3&_|*+V?ITp%0-n=F-^#6|bO*X> z!xW$J!ZJ34wS3st=Xnrxg41`jT?Jese>|VK@bFva$0yAVB!3rmtIp+^iK?x`?}$cw z3^_?z?~R_lj)!RUn4?bQcCg^TQzmHbJ$RKuP(?R>F?DAH>Gw14>Nb9*G^LI$VH7T2 zAZ0(Lf?wB!(A0@>oq>;%TF=gWM^sSVM^Ik*t8sSo*pu%l_dM24qwpG~!WsveJZW^3 z9UQJ&>-T;yO@D~mV6L-14Y3N}w2@M77g_}*nr@t-{Ji&o5dDk6gEK;ty(7V6Se#}r zwpT;QG!z?tTBU+^{b4{<kO@<#Q{fFgC{IcRz^VAvjpY0NAY7ReZ+yY88AGo>L`<7- z;G>vqx7E^{Q0@wwEj<^`9?@|jY&<rQ4$o;}AX_+*w|}#hgBNOvTXhX^idyrVT;7zN z$}yt$M}cq5l)TC{m#l|nDr<2IL|w~d0Db2nGipx4^ILk#K!h3k_U_kTJp2rln=OAJ z@@r-&=L^Ym2N;@NC9wVUScwUTj_l@UjedLdGqVIq6K}2Wn;Zcy>2c6U+<jYoM8Q?- zm@1=EQGe^f`)v7h(XXv_TR!h`Y#x1IbY<!?Jw*Q{AzfNb7qwqu{{)WjMa`Wh!$>ZQ zssC)p=3+&H!mfh~^dWU^d6<p(fEJv3N`~R9@Pa(3Jn*H{6kB|g1EbW!3}5#^mxvb{ zq}bMMypnRbea-nY85QQIP!yGZeUjKGw#^m)i+`=yH{AwUI~kEAr637He$(Cz;G7|j zT4Z2Yi)r8BV<WK+2NeohznGEGhd%hDb|0fBge^}!wq@_IrMmmV+h!`tXP5@lH?((e zzk938RB-vwPkj|C-SaGd#l{+|_uF!dO075+-*%H-3|OD8F1?oqPgDJ81z1<*@Wm@; zN`Em55|L)w=a|p5zW1+%wFG`n`duSUBJpFrUrz;Is>jWgnkR0`#rJ(%t->}fvYg3g zV{#~YYK0dy`=YB5?A=Lish7T-FSFk?!vX5zy)VJ_e4k<a=P@5yMfL6c1kJ=JtUK0p z=)m-FU(Nj&wzD|zHkoJFq2z{bHO#d49)DBm4*{&__hhOOfr-C)!D>aar06?edqJ&y z<~UwmA2#f2%d;>x2z-haCO_@=rbj8+&-|oMkUuY9|JFaXgRn^VUE};5jT-Hwa{3zb z>_yQ&z2yP(;iVsn#7vBw6*8Di6TG|~=#NZbAY@m$&Js68$klP|d0Ms@-a0(C2!CjT zL6Ean7$6)d@`ieZFp?imPs+F@Ymd#K6$^4C5xN?I7)V|zvM%gu-{FmR^}>RUybyM` zHZL%fxHyQ4TXZ5HfEV$|goh82Ke=U3_Ko+|EQOYZn(xtREE=JdTaeK`fr<<iFrkMx zN6UtTH*t2t^)n5N&PN;2Uv2@FgMVu1j_bgco0H^aQ1$G!2$UDS=6XaJ4Dwk9f23F@ z%1XI0c^J8fq{!O$W5ZZ{cEOTMDUZSf1y<i$9<6xHHGqBRtyc^m3l(xCQYLLk!IQl^ z>whb0>1|fWLPqTAysB?!*s#!(;Q5=OOrGRYyh}zy<59Yx7g+seuJ>FjTYt39Ao@)N zY6Seke~OcQ&$w%H*4R%ID2tgidYrL$)GdOL0k!uf@2U~nJ22(3;r9mb{%h&gaRnVB zt!0H}@GIezFtCM`JAX<BTJiNbA469a;P*!QouHgnL~~3ju6V+{mAyn2*GOas84tGt zIBmE`DqU5wGTTlW6pCz0!+#{EqDeA3@Q|4R5!ybXpS(h~^ely6-iAKYT;ln);l8`o zhae?#uZvWT+_dN-|N2#BJPn}HI_n7QG$pm%Kw<)a`xcbx;03>E4^E#Ci6T)yH0+mZ zK%;s`e^>T=Z!Viu5rs#4NAFC)@TN@<geU!W@|}!_aVGpI4u(CRD1Sie&RTWgn5X>N zrO!_#a)OC($^#*>dA={d6VNNy0scd`06q@n4~2Xd0r;p~gklhA-{+UB`;3m9OEDW> z5!c9tPaVYMK?q&Glkj4Y{Up7}p~Rxl-){}BVG`c#!120~3z6a#Dn}PiF9t(s71B$s ziqwqx)BpiS&cTXmK7ZH)CrH_~@5GU_8P7F^WYLI=ia8s5<{h1n?KS#DKbAqJSYc5o z{~&9CkllLEMQbrKsn20S^s5O3fpUps&Hzt9u)h+E%SZGRs0p8wZ`U&nCfv`)N^t9r zV2rv2Gj+MrHFBm=OryG1`h~&fLFa1*qGU;U<%8x<W+8e$r}t|Z=&65FekY~tYV_pA zlchVcPtw>`UkRgm%IA(XN-URxp1=6G%vMz&NAo<uMFx~dh3LHt^H(*t@k}H%OKWeG zPaWe${5G&RJO{ajAX@1*S~x~sp~AZJU(Y>Eq~+?4Cq;qkq}augS}*l32o3YUKWBIJ z4;H$<w<b*q+Al7}&Ub%v8Cu`=|KiZ}3h#{Zzc3W;g45+WoftsQsO8$(KG?h#eh`tf zu*D)$zzre%qXcU2IxscYON%OY;R45Ww@TjKF!w`n(0lH{Wwg3lq)K3Iu*SpH%6c<z z{QVSAdZoqDf!29p{eG$i#<gO)q2XLU59m~Z%EtLueo_IKHT-{^S6VoHBGU_dYJh{- zIA1XAnh%FYkVGwtPb4OB%cbR+el&b}b=!vYcy}f8u)D4Fcsx|MFy4CNbOuknPUBUV zsp$$c^|wIX;pLSZ`@aRQ;J6lC|ChjmYgVSe1R8;#_CWpeN40wxG^;uvRM)W6&w$6{ z+cly%Tiz~&Ih%ht`XC!6bPKDG`K4tiRMsdZ37RhMTUJst$g20B6rPzRS^L?_H%dhl zDKSi!@v<QbD5Y<6ylo$MTt1z5&`2SExJ$WdTIR8l;5@Ov6T^7!NTbw$P?36l_g!Nh znw|9xg>kH%XeI+}mw;6a?p}!tDD{ui_tw5IY_;Z|m~?-?Yf#HD?QvBXv4yhP{Jnp* z`#1hmEfW1pxXJNi@xlIw^fKW5h}~cXEFeA}T>+T4p%!yGRgiz*B0=9~gFICdC3>5a zryf;4O_=Qd1HfOsoMir=G^K1(#6m=c6t?UO25IntUo#OTJNgr$^m3pnjmM8%>o{6j zXqolIm=J$$`C_f(&B4$+7GeK|b9ECP2K?B+?xzLU?&2d{?L_-<*`k{h3J1{(?&tg; zD&b8B5Hg4p-HnfMpCPl~qbmn99lXzUKjj}_q;sF|gF7`Ids|wb?&I7g@Q=JGxbcn} z{R=6podk4Y=+)Vce-XSDlXjq-B<Am`P??>5E<Jym&)GCb$qy!J@_`hk&6HHYI4dD8 zWhP7%k2jWQ$A$?+%eyWS`|V#SX@%gx(*i3bsvt*xB$onlNAE~|85Zcf9Yzuwkeki$ z5vPMmsceEcX4&{zB}Zye-%caD6Vt&vTHru_0M_y-laT(!a7Z_8Q}H{LC<lV!LGbYe zMPGkr+0}-B4pOj6E{dF1EYF^e;1?owWra*(qbEn)7EXPM7`z^=sX08bkAeEK36GZb z=p01nJM^EkedGY~S=oKOSEgk#m+ly;Q%Enu4Ys(?IqW6GSKF9t+4F2zJHdHE<Om3x z{0uo<Ovg`)H)PN={zV{(8pa9z_mm05W3GSxjdK?d8vFH?p#3|1wp#QMhu5U{c2<ai z_-9YMLJuUwVM!RnyM5f3Bg7|pXerUmP2<~mh0INmkVFL?JJea&i1G1N@|&b5B>_9r z?z4>FF4bT*hvg{qM``fEohAQ&FM$6;nVX{EPLu9&{j;+nxtAo%15deAI$_KGw6lMO zQBA}3x-@jsYB}RhB9SF$wAgZW<6ZbGWG}MfB42($>%O7j%}&^_y7#n;{JLR*1&p&d zuA#yFj^m+q{e1s|8a;^F2(u^29PE$Zm0Sp-^rXQ3PQmq>vH5q+?U|EX!mhSZd%_Gi zUvLlX-w^Sab|>lnr=P4#W|65Y-p+qK-3j>8+gDI_Vs4a_WCl|GJ3a6Q<?rgI;UqDS z?BB_O_2hiF)ho)qF*E+1!PqPHQy<etIpBXMj{V;+kI%nn+SdH6>GtpR!<W{-Lx@+4 zw<gcO|9&|4JJ{bHADeCcXUO{NRYb#YJg?85O^*Ht3TE}+Ryz0p0Qi3Z{9k_n(7!y} zu5$VhYHermQByVxOUtR**?^xvksn`PTy?i)L;sda_j^@(RhPv7bmH1{AtNI)m-E%o z?QKJ9kkNBPBO?~8MP@NEu|=e-zcXc47>wPb5f+%4!#`xUiwnU3EEmfAy8NMZ1DI5Y zr>B)mHESR-FfiIxza<3yg(rXVAKx#mPE!4cqRz!T8NF0hRqK*iVuu2pPv*0$tE<HY z`Tk~|o^<~Wqcfp)zwH0H%FP{cSU5NXtaeZ^Fn(!EX6&qmlWcJ2&-5{W>oe=*Pr&ln zmyP}h?2^!h-0g8AqocjIM>8l`SO?Uaii*hjvdP~=(WopyH8eGG!X<xx?eKB_J)k-0 zcj-~SHTw0R=JQiXPA)>NxU!P@@#)E6G>IkUQ+@ywg+~zv(Hs>qvCvI{+V|YQZSzp; zx2ZHIaH{;nR4++DEx!D6z~itIa(8bM{gldX9z*F-#N9O;@Zjw1oFdrbV;%hQU-Jm~ zJrBmNQL%r{Bigt6QF(vdio-H?c?F_!b5nmYumX)r{+oB4NfM8|<li9H{Q;4|HeUkt zpE)8XCI&yx9<y^bkZF&GgaqXM{T=aR^}kFQ7c{6`DyZn6+06F>MR8l*+?)oHfF~DY zoY9vix}?2*U1{_3(hi<TV6|!j;cwNJlL=jwoM^C7{KIsK$eDjFW~s}*j)NwlcAd(@ z0MIC9?PNV{1;=gwYvAwAYl$Tn8^%8eQf7fyCM98`V_;-AH{;65$t?#wSQHvrSkMOZ zKHII;eEYquwanj8odYCCN&b<9k%*b~J3kd75&Zr2fwU9mqD39Z%=tgD*b{vD9YdhH zjlcd+H>`#I5v6}hG>Y&aDgVD@LZSZlVn1dajQ{C|2h!hhOa_=_AzfK<7?8-8?p%lo zx9$O#xi?qG@Ar&qwN?vL!m*d#J$tXeHg@oCyWTSbAmG|rDrRI96VAiw&na^=IhT1{ znISS#2jbPJItm?U7e-{PoR>B0^Um)(oA6qBuE$NBE4zPMk2o{j?-7`%F4!E8Xw>3Q zw728WS&su4Tn=n{y6<=u6=(dJP^}p&S|lyg8=|yt>KcJtt25`DZ_jUTChJg(>+_Z| z<N!cD(aF4Rcm7)N)=8ahn;DV$2e8(^q=@X7<-^^aWCjdR49O1Ze=F_pYJ0JM!RL}i zV9fG`E<b;fdKp-Fya*Gw&|rTxw!P=}7^qO`^&ot@{`xt?XP*t<()Gt0nUM3GJ$_L! ze@`+^SIdRB-&srq!H8Op&1In5W<kwQ!r6MO<Pt=EZ5xv1E4}6XkP+DHpQ(T)$_}$` z73U}WxUV=9V$)~xHBN6-ZU?ErGsJ~-(4=B%B~^cKQ0o@yxXnANjb13rYsG{H7~76p z9<>DLf@vpjy;J9P0UKWz296Ycqy>fmGF|@2Swh>I5NEeHD7WpgzCi|_a8)>gzF!|} z>rb5%`moFVo`8tD^JNbre*Y(Hnd!6U_UB19VeQ39r2kfQ8Y~DuE7jZ4S5TK=W}jvY z(*b|8o>sgI2D<*ykqJNZ6;Q8A)!?*S>;k9o#Wr(@cD-hllLtacyAJYO7>nNdJWIRW zeXe%hTJ)2fVq{J(928Q9V!#F#unm=N-Y1Gwg$t5GLWCx=Kj-XG|6RFsjKMcgfwkDh zMzkmoGq|B_;CL+$I?>JOd)$Opsdgjsjk<qTmqEuj<68u%?iVAkaqzVGSiiKXWUfk6 zjMtzj;h0#B8RcfEdWJqKKzYypTVBk7U(A4()$=}`SCto?3jv${VwS*ViD1Z6rdOaX zCsrpu|0k*FccDG+(aa9;4eRw#Q1?*6pQyph-y<G(Z!naD`0#7Qh;{eI_7SaL5P5&` zP$^3FBC)H?k2oHwC~~}BTnBlxdmiX=PXl>+InfQ(RdDl7`<MM)o*L{oSm~Enuo4UK z`v+2^x{Qm2fp(}d2*J81jOB;`U=zla`4F^{su6AUjVDSvekczRe)>gHYR%)IX|txH z%I%CX?s<TeU*KyuVQ<bJl%j4uS|)#!POz?IN`N`wo2m_&eldzvjRI_p5ojI>LBJ2A zrT%nk2_pC-yyMxRADdLx)Fe7cj#m<`_wIy2;n*RcsA2|g>J7K1<29Nnh$6O>nUua& zjfv1h&ok&oh?j5}npLT^uU;c-SK+nLG876Vg_)$@FKx%Sy6~Fy=0wdCJM4e=4gQ$o zwO0W$!BF5W<VYW%tuQ5KcUEMYwaH{!Q?Ez%wG1oO10!zx<amcrSzs1*+cM(re#L zG+OhJgWi?Rx~2t}2YD$jndeXA>orvZOiJprHOi^XBTt6ZTG>9dOE+{QlZ3%gOrU!z zqsi>g15mJ849UCUIP}B3sr!F#9W5}dWf$toXiWVlnO2dYI2iha;T8<lLkWwH>>1C_ zMmhcVd5E?fDhO6_tAJx_Nl+2Jb{}9GH3LYoR0z@4nlE<+4(Y1^gU+lt0aB5a%J&_l z$|+l7)@QCdPg|YT(>Gwt?E}Ihg*|*M*Q`lzjjh&>L%Z4l5^UcC9^8MVKA#^$$voLZ z;q8d=%U$;kA<8-@!OjG3lzmT-{_7!e=`#nBW-RNbQ`k3^=4JkzuA61Y{gVY6s7TLy zu{i7X<2?b3HbJ6<mm5w8pLGrB-21YDn5VExH*FO7ddq@3A2p+q>O76=GL7;Rpq>5_ zsaG@U1NmDUTLugy$>o3KK4wJ)A@G7d14buP+zPm|!`&6i$@$-9T2?Ts;>5a>Jh(Kc zwFGeXoAHpg$X%6koLbY$&8$8^n2fbQLNvymO5pX;6UMrKd~Cz$of86^-G&}DeC4XY zH7|uiajZ+^t7Pt0TIiLb>Mi#??Mc`*g4=7)g}xMS&{k*{bMk+v;y%7A#K07j89wBf zN7oI_lDFZGC;t!qsVQh3W(G7#ET12oj&sahVBb$|S>ip3E2f~GJsy<%t`cmH@{um* zw?z**9jIeO-pF<J-*|2Hy95No@zW#i(lw_`ds~=BC8chub$+4+*a~>T;aOT-ZKc8~ zp(X|%p+SkNGt7Sr^ryS+#X5%!CI?92L^CHy<D{)ltr9GH)ONY#5;moB>boK+rZj*7 z#y>!zz+UtpH@)Ra##i_AFA#~<0={>4Lc4R%8YxQfD2v%$>uX?yF7vNk_Ssmb7RF%( zIJXRMs<*=7wXftONZ#yf1}y|02#Dh5EBxpg7bn`XRt<mdr?pyc1%-3bU^ZX=g6&$` zX}N*fBE(f|gx9X?su&rH5!G9T2ZNnHr5?am#HEclZ5H0Y+biL9b*{Zg>LqvmUESdp zTnB10#m;TR06aIUEB98y>*i{~kY>>#^>b+8kl!-0Y(@-N&F;yY-0<_L3?V$^Vet>e z%(zhdQ0ITg{n&}0RGKF)<z5U9azHz^=8#50U<Jra1276nP!kTgjciOF=sDkVt63AW zh^I#1<pSTk^vkGH^Z4plYmm!jef^s)R@rwN6LiX6)$K6Ojj5ojy5t6Nn4vu`WBKkS z?yE<=aw-6ZR7c4xp@MfKCJVO;Gvzb!dd5rOJf?p?K2{*B6^*$_E~Oaff_&=CoxZ8W zUcJKQ{4{HLDhJ9Gh0N9Ii)RYiIimKs^SZpvdUeI!30N1G_H(iO0P5?Bit0KfKDS!v zbqZx7v0l^i%ZrVmYzjMi|Cr3>?tw*PLqj`_b>ExTTHzY0U6xqzl*+Eh74YV+`-Hz6 z`4E4(nby&@D;LT6RGV$3-$_oAuY#X3kN<{u{K4wtLFuH<rfb@NFfGY{Q<U!V)e=rI zoppH`Y2yhOLmA%L_St1h>xsmiy&5d+a^2F_D3AqaW}r{MCr4pVU~&4ct|kMf6#!^^ zXjAc|ylf>*;a|~nZOzYoG9AGGU&-)R2-JTXPk06l&c#BLW4mQ;51Wro6i&c+AK@`x zQj8^u#Sz@$A)b_;2evs!^m>y|>?WS)#4vo~C)VjL<*Az~!nGLL5H2@_+1}pJWV_;R zyz4@1jzurQYIy$p!Pc_*yxnj!9pE-M4vwh@{O-HJrj?bK06<S)rt(T3ZQ3D6NfUoU zEa+29!w>8j?$Lx2*4Kl&xsaiDS+tY18r+kN^DgDM3a-dp9Krrk*;EJ9^(Z76_$1%} zet1=w`QAts5(+jR6KfLpr|fHodJ_Ox(11pzr7NL4JP%eP0G(cnAfS3~xuyj<nXO<8 zmBrM-#lxS_gXYKFQ%}}{Xf3KcXH|dav@5PR>x@>Ab?T}2_+*0yLMc5`cWKEEGA8;D zjW<Ymd=`A1ACftqYsvOgao<`iwOG@+aZ2Xn$n2lRp>F}C8F^maj>&imM(ZN@_2fH= ziM!vJGHLR6+^k|-Z}aKaG^3W(6(sZRq%y0}K9-C1x={Cw96horn9e1tmZyKW8@K5P z;7&7`+9#JMkp}X@4xEyH92kt9OB+XJghE}^N$P)a=<kjeL644MnX}=0tO=kGhow2_ ztysxonntZe@edTE#BT=sLj)Uy)7%dB)Pu66#7qoe{DP8_eG~iS4V5<?Ig`h?Hgxu6 z!|60_6HBh*spfvqpGQQI_CkNA!!kS%v-%QwyJR)D%TUy)_S}BG2dKo#+{YxjoUXr7 zTg#?#r)8XS%iHXXOpNxdMASU8-qEcw^G^uIvvO~=8?Y%A)S~J(Wt1g(Loq8C`6XP+ z<QzB{|D3LW5}PXc_$jzsa;xE}7<p`bvfrb=p>F>Kqte5kMMSKZZRCHa2~d_f<=Vx3 zL70X56mz_3+)`{--FZRHh7f}JqU!GCCYv!mxq*Fq>Y%}LukEm2o4?g_=ZDg8>g_{G z=^L;x8#*c^(6lY2C<9mn(s#aKv4hxz>e-@qsMR6))usz>B+_U#UY)C)U>@GHh0-nm zq|uvO*OL#{O9F{&qlSNh*}W}Fi!I6H-6}lc#WMfZ?xsS{KU<e~48ia%{(#!=IMb1J z&m%!E*u(*;UATNj$*?J{$}RH}3UsFJ?qS@G@>6};gEx@BzNoH!A<_)AVuDn{>pgVE z@w-3bpH1tsNY<1_<n^@N<wKN^@#vSwqc!jJdsHc-U$5O;7f636MmH>_S}y22`KHVF zZQxuz!6_;9iwrY8=)-KIAzLViiTa-27Dny3mX=#Yq)eaBobp=Kw@c<A4h3B>^G%8x zen#3S<Wqh<IFmTb1`EOFK<$EA+Zk}T&9kf?e9`EGLC+}c8PqT!I)1h~V33u>FR`H? z$oL@#9qqzTZAgFq`X;|#-j=6L=S(ym*<8V;5!JIohmwl=<B`N*a>{A|PmRzL%FpIL zWjQj<HAp?Ru2T+&0q|v(>6(eLMCkZ(i}g`<y(o?)Xv`c_=U6QdDFzFH%wkmXGLuTM zzw6LAd_27NnN>p(u<|Lrs*7;+%9@oXj1-1RVM(hm1Y&<2g&URZ#*lzj3ptJz9}8Z& z;&NDZyT@$3r)$^zam|UUON>{Y^vHWVrF=cy!X1izzC_Y3wWK0i=SN*4enU%2kF;-* zi20R0`(UUrnS}mjryV?jI~-{u<G^>Mjm`t5<i?AEgUx{lok(%Ggb^<hCSub?_*E8} zP~tK99~OU(CZxH^b*#x*{^9{u=i8-fU^pCCii85an(l4})DS<$aHFy?Hb@${K%4rZ zbvRgM(J{2m!|Mq7<m9e7C^3pH)I8Z0O|a;Y2kGJ+Or5o8SBlhXcg8<tQ7dX|_qmKG z$j=6v5O6^63M;<8Tb+$^gI2A&-$vYp;B%q-8^(XdOFK3^Sk)LJJa+NX(?<3VMo-Uw zT|wTO)+Vea&($j{pUPueM0D}5V5Q$M>pn~^wVu)5P&q|_RDCE8YF%ilBOYd*KK?EO zYT%^7v5dIfp|yno#pE81SesQlzbdd^+_5~PwBgXirKbjy{`D>Tmt^i)5UC`6C#%&2 z`HFvlCj<arzT5gi-lf>t65P7XOu;msOR-!uVo;Ex&jA7kKcs4AnwUR3m1}{g!L;~n z^?FT)1PgrOBo}(E8Iq>04XXw6vVU%>r>>v!9ZV#PEmH{iB*Ayr&(qLue%-3J5_8$8 zH#m8Sd4Xbx^4R#w#k$#Q$?WL6@_E4$?@oWVq0jq5YnQFzz+pmdL6GFko2YJY+*{{F z&_QlSr_IH$=IJMs=Lfn1Bnn<xIpI97XbtB<*|-;_-L9mAK5)L!BJZN^Cb{#ZCM59y z{Gi(*^>?8t4k~Agvsd{p97mwJf4Osj6|ftgX}I|_6@MUw6SwO5Y~V6T*3ZE8xl4br zbH+7r^Q?iBOr@8JNx{jb!tB&@Z&{h|dY163I;UVnE_v`Kk<YNm2A8^UF5`ph>cc2c zL94{(J(W+Wc*i5z^P4H7&6gU0S$b=2t@|a|>pNg$(274+pC<a?5D~RQdZDcI+>9`& zsChZs;fn)pHK7N*us!Q?_xUl$R{MX;dKbe4e-KKFbQ79S%NdQ>+9UM20G-55-QAw& zW$~Vw+x~p?y`uF?IeqVYzj1C6)`;5bj~L}w=N-nhZ&s#))O_Gk3b=mlBsQSC(G~U^ zAHYLw7(*CAI0b%hvJ{E|*jD$AiYoirHMji%hJ%LeBEpIbH{>Rh5dv<uFD-u=@m_GI z<ev2>dlRc<WI8Du_Ef9y?B1kgp^8l~ACGr@rwfLmou82ezMF%kY)IlquW-;J&h5-V z?FRRE?^n?aM?;Q2&nUv<6P=yw-dk4fCU0!E0e3EUXU6w#x8s97K7$+=$j^0{#aAQa zlxc<(_RNWUR^|yIuvxhTwnTrC@Vx{Zf;eDSgNhGFo0-iy&lMb3dLW=!i<XMeG60#s zsMCw0{<7~Gb7WSbcRHPz17q9na}2$|&ei)dOU+TiNRH#bE7qG;)#$e0be;Tz<;X&> zjP=#_+^4Fc0~#QRou6@Ew;B05pWr6NiH!q;`IV5*N=~{vNhCI!-V=Yy%7lHWYBKJt z`z})NC5?*x6~5HXYbZ9Zxo}`<0GHP?gxe+-7FjVVR#V>fwb`jZPU0)FPopXIu!~jv z^(?7ts)p|um+vC<)Tkrad!ynnO0+rZ4~}XjezQZr_-pp#v)yJ!UX5WQ9yeN8tv<V_ zE$W2Ubl^w<-7oQzB87j}U^J~KQhE`SWh98soY?9JU7j~aC%u{3X;e$yQ+5>R_?azz zRq$wg`oSVX&Zxd?jR{)~JL|5MRZ|mDR=qau{5VP?DGt$8scmgos-2`gMYZf<SpY<Y z703zyY<(=5-(x_!e{J&NUCvJ9s^rCTpLS{88{yY6JH>k_lL&tW(B^Aq%Ggb}FDSQ_ zj7kv<A}MZ%8$M}9JUTs@RO+@eu*%kop|Kb+_MU4I1R+{k@Y;Ivs%Y9dfX(a}T!nQn z9fYao>rgSKvC8hhvKp+IRqWKfilMac6OV6gthX=gO=OIcnz$7b-ER5YXS?X~U>!ur zMq6g_+bmcX{6c@=x}I#pJ9j=m%(NNuIN0AE8rSY^gLfBT;`-Kx6}Nvb(RL}1T#;c? za;ud&uQA5;)esoMTS-Qg(8ykezkZQt8|0=}l3u-6O|xKU`v9%7=fZcl3`_pw(s^x= z`h}r88gQ`yKH&t`rsJ8m;^hdiE%!`(b-s>#;eVNpQ2~EV;yl=EOnhI@n*cYAKWn>i z_o1|#RfSvlJkfr(1UY-RF*Tk(yrJ{3zIcTVbIS-SCx@_Dzn{q1ok%)K-YwiUeqg6R zfk!Ch@bO}a(mrfIW=q)9>V9*%+T+vNCYVCqVID|K&ZYcTAGka$J-d7%KF-2EpELg? zW=#=EWNUxmM}C%eHvobmZn9@zVNV-|Z!5)3)#3rfd4D{V$J&xfXQD3cYwK7E`ZbU$ zj%3NT)Udm$Fu@;8mFcA{;3Fyj1=Pm<a-wlr?$JSE1K3eq3;i5da&?nx+kba3;#jqB zp+vK1!<fz&(}L9v+`v=@##?Iy5`1_Kia#n3Si*nZRGoEs>hpPjJ=4;?{M59eW*Bcx z<2I9Vr)bN)V$7&mQquX1AOVkKJCZs(W&fn4C9}?;a&9sL{GjowU87;p4j%`UNDp5I z*ERh{gs0;|jNx3X3PbW_w&G~4yxf@T`1^Qw568U%|28f3nfnV(X>qJ;joKr-bW6s= z8`yvEG16V+XU{j^52`9JBlf}T%3yI&jC&7x3A<Vkz5)f!G1_-myUp~3x(10Xt>3bV z`nJ*~*U?kfb@vR&1X^X*>Cyw3_G;Rv1D-mT{JIg24<yahL(FVjkS;KWdQ#8}<4L)p zwq2MgCaZUsPkg@`o)L!ih=^hMe56!dxgURhG2Za5_m@~66xOIb>~M1n)36Bu?^K`{ zVy6PAu3!nB!=wy;c3Vc8lpIHh(g@_t^b&3bC;jSeEgx6q6P@hHY_Q^U`5Nn4m*sD! z6GjYk;jlFAogCS@r*68fne2M9B|dLF&D4s6m8=#r7k!Gw*w>@LSl4IIA9P-MpiX~j zc^_jlPEk|2Hy4O%S``4(m8Namje%vhwe&Rq$N=JRzoC1uvrOau7^UF|%<iLqKW~JE zu<bCHQ;CEi5AQ9k1a0MVt<vz(&@~(v4=G*>KESHzEIjN=;`d<xzFxPd!1jbV$p;DX zW4$;d-&+2RQ#)iS%4N!RPyRHfDXV|nRpU8ysA-LsfqafmG`?mk7Sdpfs)gCG+T#oz zMGEd0V<U>5`#ZB)uzvvsG0wsU<(91HY7mKLA2oE(#}YPTRZZw<zbd!;W2YJwi21>m zI_s*<tHfN;*OeHaBSDylFWeRm3p*p?bs<Boh&0PMRS<YdfrWj=jP>29*rb1?;vsYS zT{5W=v8>^!HQP_N?Y<G)Z0x<nJ)U`1de?SE+Pcdzj>nuvl{)lbQ6_zx_1+mz)h`#L z7X++kTBn2iB!Fl#h3N;aV%L>cr4P?7ilK7o?_K7N?oQ+uE==aUw#mU#uB2EB{53M= zD5e3qL-@MUDA2ZK7nw+^)}w!OMeS(Z$0BJBElmvH-X8bgHEJvL$PYwXjcv(_*V1fj z>ngNYbS65QXCsy%1pASgG6K(7ZDa1VNY!I&aUT1g>LTk?Pa9o08vI_EzBr9}QJpm{ zG}ZYuQ_hn%w2aS}_nC&l&)uuH&QXD50c=_jnG~-A^q=D$b3f*JmR)}xmq^<j<HItZ z<y=P0KDr*Hy*WnsBGr+ivrj!LfNU#oTEH3Oiv_qUxJ#EP?1UDH7LEmaZz!aakv5mM z4%m&A=bg8mHUJvmbi8N4OWncvt&U`BMfJiBf>LC2$KcwqEX4-R_*+Jq?Uul?`|8yu z*@Deyasc;(XKb}wUGsnbi-?@xfpe)!225_0NrN45>}vYKCZ6Z&UX~fE{&hG?Z9M~q zFGKw6hKsFd14jei!Aaw<^Yn};tIkJp(6&gSrK&p8ZZ2)ECQ`1-6AE~2r#g7QTFrx` z`mG3)x+I4W9od>tq4(-Xi6GegjOY{fZFSc&r&l|c?Uaw6wV8isqmz+`oxE4#Bx#ww z9B0gT8caqA+gC_O^_AXFBcE2^AE2ViFvW&1bWn^=k=9s}caZCR2=3{+MAJ~IZAKCR zkTU9<4;>#!(sQfi*C)q^sf69uv9aue=O22&6JNJzC?uxx-7oz6KC0o`<wS=b^P5ou z7GDayO3Oh%Jhy*g;NM=1l5$o80BaAnkCL;)_#><ZdjwHC^&r~(*N!sV7OxwtYYq>q z5i2&jp)Hv{$XC32@kV2#RI696ujDOKrOEHU>9OZMXbVV^N5@oGFAI2=!HM3#-nh<E zSWwR1UtsBvu}qlC-@mZ@!14}?JMp&Q#S3<nIJ!JO=J$W4Cy#p>!wfJp?XCq<xESyR z&7TdJHZ8ofv*CLRc*85ukBZt=f;c<VdV}S!eQ=c@)lAh`0TXtT5w1Dvgd0CxKb?CY z3x`8rjZTj6nsm|PTpx}Rlw;5py&Aq+x&0V9>Lgjh_57SW-?#UHr9cnYFSigC#Nk{w zGPIwrYp8$Q)=-^ne}i)}uMVZxf16u^LY$zFct8|-l{{TeK7=e;&0BS<Pi^ydCN`!$ z-Bg48nxWx0w@zwUNS144bRrq7=qMYY!Zf}*8BkY&LF{f-KeHr|<UMd<r=Z5|{V`G} zKhPuvn(^0(rVIX|8QTV4L-&u+@v&H0Vr8Zr)t`SmQcDSGJT@^<76YMLtuMQ&%wf#V z>7!4owlPCy>D{vncm~MMBS$|mqopEKcei-Itl4Qfvs%dqnYo4aP$X%4YfG#1GBRzC z4S9qDY#{Fp@6d9;a{t}~s&|xubH!`F&JOlTfn#{OtFDe2%({p6uw2#i=V#okq4vzn zYzBW8d`X>iu!XY(gHsQZr*W_4<)&&a*@NI8)dRVn{mL{iHD*0SxZcL7Wm_{}zm!mK z8?|w?QbbVfHj7uJg)`yVp4RQWwLkQn;n}uu`UI~4B=q&X$P6T=i9HTeTTpIVbswql zCz|LfNH}skgc1!13?Yf+i9L`Doyn!7aqWLVcPqI`;F7|>o5oDrx!a#>0@hLK+KztJ zu^Vn6v#GG|)V68vs^V}^VM0FkFoRYFt)8QyoWDQQLPRQOHCy=QsIEUx&_JqP4ndjt z;%BB^HIQE?nrK<~7XH#;5lqz~*|^_8aMWBY8lP~6yYqD-ve|VaMd6G$eyvG|(QAJ; zbiSfOtF5^f#L{^EImwZAe1#VAp#8wB+<oP9$C;G+jisgM*jYb~KtZ#ue3Ul%BrnYA z+dF`(^&?#kPS9|=CM2|>{k4Z&^zi(d)5hbRW|s>bRNL-sl~1<ugGPr;&?15H1IqhB zi8aIH8F#mXM09rEWD`kY!`~P~UMzong>bBIO+la(hO+7615%*^FYJJ*1J+Zo9ZvxZ z@crP!7V@=3ZL=Kk2GwKpS^_^6Ciy<#({1!(W^fLMcJ*H29h83X4&#%y2JlXN<2?6h zMfLGKBBTnlw``{^!L>hfw$x(SsSLMky>zRgR!V#UJ@;&eU0COy;Ru-Vojrdg_p;mQ zQM;lOvQG+*D|ttHNZ$a2wdB19SOXzDba@aIHcEmH&w4LR2Dd6b340JUQ3^-i8sIBd zBLJhZZq_FJ50REtSAidfNzmb()Hl|g3mU4SbDVm+F0;;@USB93-k)-I^wW2@+TE3s zI3G}}*q=W`zCEFQ<$LB$YkYsBST*#5bZx_=5Itw?N_Aj7?@F}W`-DPrupZ<i`6}Pr z7>B7zl;J7Zo__1Pnz%zw%B_rH*%l-*s#iID6mnpy=ES9!vdkBgXLAzPXnSrNJP@78 z@7rREl<+3<x>FyF;{3q3y8i%r{<cq#rxL6_&@bt(V7Rn3zjAj*meGHAc?eU_T<5k| z{!+PajG@7>wbb~*<f>n&Hc_!XOqDr+E_Yho2{#QXK@Bn+_INk{|55YSUvULpmvBOm zBqX>bxRap4U4pwq<KDQt6M}mZq=P#I8fe_z-QC?9x5gPV^UgcpJYoKT@0VJOb?eqW zb!wk|_NlHS5?`@ycs74^_$Q^caNhLlC4{Q?9fr*Xjj*yW<=V8r<IzsMaFC94Ya@$j z%fmb0+_9sj-2xD|EtiwWY<Zd*RlKuU`5o$|o1Zm~|9(@^GBtKN!P2CZob?}?>bt7g z;%$4zlW^uOhCT#@TD^=8`9GFT+Uu9RN1v2XXYJ_awO9@U^Fn|A7}$ivRbXj_n|qkM z?sd*P7gNSnRsZILW>&J#8_u7{Bf&P3=M2gvCX|{MUbznXgm0Ep9w{Q7X67Yd-llx9 zb)k>zkKx$3NiY9n8IZ?0Yv?oAHlO;j7c^Z^omTD511jR%^*DnPfWnqak1F5S0JH1& zt9~ZkY);*K<o$nkb0jL<2!_ANTO<>C;o4~7(PbGu-a77+-f26DqGR+Fz)xW#F`YwL zP+?WdGwC>gwnuNuxA|LnHl=*Ot=l8B5^5=3$U7J5h-WiT-?_Rs$kt3BcoJV+zP#nK zj+LgoSfAU=hq2=gj~7Y*e#OFMbVk2?$b#RP&|!<gCqI8YnDW)QRThSjv67=sit|;n zWUlm%>j}2ns&vC9WFGE*g_a;*D52b>O^@zZR6m!p(F-LiQ_FU0V37SaN|(m^dgTOC zPv7bm!~r;?L2wGbQ+Qakyn-TfsRHPfc|*M8c2MkXqVH6x8?+?M(dQmiRSE`pcs^ca z%tr^FZA*XeVR&0(*$gz4&iboOv1%J&HW_P_;GD7Ka>ZhKu}A?qHz~=yKI0~H;cKY- z-&3}X1bn{gA51#&L~m*vYMKTg=d`h={!!8^s_QCn0G+i8=aS6ap;zr|MC!zUtzjm` zxmXuE8Sb?FfnS2_H!FZU&&cHQYu?`6Jb_LhC+dIK7*#@phUmD%>_h<j$mb82Nnq}g z{n#sKW`K>;1t#wv&fQsBWZ9VOYbjU87QM#{&ZXYp;jzPyQNl)tZMI?(_&?E;;?;-< z3-B8&G^!&edPJ@U8Q?kiE<Y4wLm_Ay0H0oD-`3`b8v%6vn0Yp=Qn|sj@v!pW^k4dt z9f^O|Ksa68V}IgScqjYcRE%wSawWK63%uCY<|K@VcQ(%>_e&N0W?jWsiN$G$!H~GV zO~qP8jYwkMcU`mK&Nh6Bkav_SL-OS#XpocmzR?oMS8F^_4}fcE^5|^yr&!5?&s^_b zo5?^Zif1T@F84A*V=6$Y7b)zOJbSlAOpt#O`f13t5HCT%5VPoeG&e2=u3r}!V6>e} zMufN;hOwziRu-&aUvThKpn(WiM7_e=_YsYv=8YcB(93)OQEK)V-N2#c_=O!sMVq`1 z$OGNobq0PBS+YwN9Bs1KHBz?)a>y)dWh^l+7X^h<1E`IUTFObOxI!T)Fbu5PF06lL zM|zJ$LPCY|9@wpUOd38H;Z)3Wg2Ri&h2DL9iH~g1(=c}<!r{z(_i%&0B!t<u2%%Aq z)&NngK^X5)V4<Xbq_SJmP28eU+m$^Y!O7MKfxKLF@F!nia%vvw8on`eTwlvPNUE~+ zMQ<uBqGDf%`>1p)K)_XnNb3Lu8V`S9hV$fme%v%_!&$hgH+S3nX~{dc@MEWR#%*7^ z+D*qbSR~4sYlYF-NLDo{R8F?ja~PD)&M8b+dWUYoNBRI<(n5E#cKN}uIw^NL^J!nx zZDPIVUU1@@en9t`U*C}b@&P#s=9keF{+IqT*?d@^f7)z;@`#y?Nc`l;TG)T*iAsV- zC?fiHruW)=d)JWhzrZJ)(vs*fQs{?>m~825grwufHv(`Hrl)l^4(Oa9lxX+@A8%f` zo@9I!S6L8&N9<%H{u0rFoxn}!eQv9+p<>F%fH@8#z>T5ZT%eMuCaQRF#qqnz&26<k zGMy=uY3#7Rzj9<1%jWVialC)DyjXqkMPoKfDgiE{zmt#_PW1XN`qjY<%Vx<md3yRl z;;eW{J{UQ-i-TA}%i=0&mjd&Lw8nuUf$_(D_RTz7JVWRClM|fMVhljt1&b93p=-h- z)M94~Q`*Arw!OLanDDXHNLmX=@uqJ<nVu^pjmUerw&lR-H5E`itDt`Z#UIEfH%KYl zu27l*E~79kvvvalPu>K`-Z4GUsVVr@u9InbSEyCtS?R<d#m7Sr_c!3DU&(oTq?8b^ z^ff8rOnfe-4^se8&}#P4l?WrswWK}XnO^e-Ecd}00TlgYZ~`rQKA!o($2pn95eX3r z!u<yx5)&dKh}?{d8h(HGeRpz>mFmq(I50`2w&^d)X)2K*UCpc%t{N|<Jv#Cq8=}Ul zM9d~q8inN4Y>$VJL4!86gl)nY&ZvX;OLE_$*BToUO78ySBv_sZ3CSm%*_ds^{sJBY z8#^LIDrwTr2(hUvUylx+-R<a1o5QRt*z#d}v$bhE)lxWhaXfz^>`h<F(o{0L--PAK znIrP?0I^MZ7YXgJ<&z#@RE73kowR0E3`oEU_0$(eU{inKOrK2IRge9H`<f-8x<G2{ z;pUFEzQsUR>Wv=iy~U?8@lWA}CfkfLi)rTo;p(!gSmK;O)^LaR<M}lC&ZE?8!a_>W zo6RG45jCUO>%xCj_0Nt;d}J{X-TEE2j4e9R{6T~3#An>49(&FLF{{yJ+<hIFLvnBQ z+I_CYMNBq1iKnW@yFzy=y*P58o~Tjd$Qivlxj#9X+v^uZaDXKXI`2ZS?h#8HSSDMu zhr5W4i3;NT*f;L-U%Rcj=lj22c*uq|EOGN<t1{o6#C(4@1o3`eg)$liOS|h79Y)cm z1yjcZIPXx>>=_wk6ylcXGA|@UzDkI|VHlfbbKs(*g?ozeN1IkJ2I9q7*2{%|h)e@M zw6p!rM}yq0yO;Ksb4$D79J-m+RXHBM66lUc+CQNC0J^`lk9ObO|Cu8b(lrpwTyhMl z2Wscu|G9tQxfQ=>ddPiQjFbXfn2S04Y!mbC?}@-!lz%Q?u)h8Pjm|jlp@}8dL3;cq zA<`W!UMS;mq`o<7xe)mW+n>=_0}rCHaI#k#=Rd_Mln9!(?1aFSBm3rL0{mk(ztVHo z*h?d@ThWReDAJ3)i|)uSWs;D*WJq_DKiCkTPfUN)^ur}{OiU?@fm5|w{8na-ujqRG zf-ERfaNt9!o0J?^&L?Qq12U{BJ_Ka!2M4C@Shl%E%6&X@PX>-7HiBg7R`4bZe$!Kf zozmZn2i})56L6C58<8_<z!y#&kKn*bCL7*w`f__wY)P%#TQO>d@@+za2;CCKBGK)~ zze9hE!Mxm?f^qUn&YB0M*VzS(acOE{yT@L<5F7-{KD(=1sgB%0i*{0^2z%^ng4Eit zaMl!pC*a8Roycd-E;nF|sI6#+hJqene?*U*DZLXeToxek{e{hn*@(FE_3!!4zi-o; zv(P_tyMscZ&Xcl9v+<QhU(rm(3FlFRBJ+O{HsPBeg03Vh+yo6g8zJt`9rz{^BVk{s z)21fo{(Goc{2Q3hrJ36x>iN^v-{84JUUWiG;CjveUmfa*|6W0X$o(dN{(v`$9}3yi zEc^wT@%~>Oo~&2{z`&SWA<ut8BHjP*^*E9r5kdd24!7v(SCjX90bFFye&Qv@4her; z5K36^=<~-*|DG_-L;q7$)VVhaHYin3AK(4rergPlgp|hGxJ2$V8B6wPmw=2WY{rXt z9`3|7mB8b<EfD<3Hq-(s{xFu%>y%X!ZM67(aK~|~Rw8`DRjEnl)9|o(prj~qKz8dZ zeHwZZ$%IkL#*!<Q4g*G<;==X~WO0A;g_>8<0_9lcM-7+>84S9ME(IsEER5-^-|Ou* zOVFwvbxO4&?+!X8dhjA*T(`%2<dfVheuxc{?$2m^w_rhw<9sLl3!4@*N^((!XEM?k zxz+vmGv1LC6NF3~-QQgFfLvua$HvBfBqn;4>F=<u0lXhL+%Edba7l2QTit(-K_Hjn zKP$1B`AZ>~RI+dExc8)!F4P4cway#$*?;+GYgYzd(Yewx|Dq1L!XA}GDC(Xj_6f7b zs7LUtxg7Fbsor?iprdau$*8{)c;&o>;|22kAgL@)KCUmsr;Wf2dLhSiAx0Rn6;;f^ zp(eM>Wvga&^#S7JiS;pe-cEnmrRFRjWyyvI6&4T;xkpXa^bz4HENCQJ&V20Yx9=W! zm}%JYRoBfK9|8{5Ax}r_sJt2Gvt0Q1*#9vp9HReAMMcE}21N*ZbL@a=Y4%#H-eXJn z3}su8ByR|bLuMIbP>TXQxY{k+h+X?4?h}Nm6%8x&Sb;h~YUWqiw|sv@>J2XiGm5(u zInSb$_%)?e>+SDC!qzl7FCV^6y~eksQPi=OC7*E_(7KC^%SqgnRWPRee&C)D!6pB! zDoDNUAj27}CUxSEM!0K?<6L&CYTcDr?_htj@lk?3<O+)!&$q@I?fXF+h|kOWeP2I~ zLC9{fU8G}U7vUkV>Sccb&>h%FZJAdH9(;$cW>fNpmIt`ZxGQwGnYe1x6tu!2DVEQ| zwJ!^^prrRtGoEna|D>W`x9UG3UR!`_I&?Xa(@?<ofs8x}+L97y!9i3^1fHT%lnmCf zoXTXn^<veM`^+R4i0+cZrUb}P0vskVXUNT!XGhp=d#LRG-V1*OS6rkktgd1)C=8hc zU$z<M+|x7<4M}sguOS)rotHCNT7_+u>yL)JxkIH0QryW+7A|QA(7<KlX&NM*X0c~j z&B^u-|6UtRxBh$OLf7XSXtmr#SiS7SgF~+w@a@|-c7)5zuEpGMd_yrNHAW?&62-NQ zzaM#Z`IAE`5`}*@`b+2qN-=IEkEe;cA5PmzXKGjI8&W(M(7!I9JK7I#W&xXt(gwY8 zkT7Btl@$UkxW?*o!t>*5zI_*ktj87QL^(|aO)4FDS-BYz;SgCmBqR6Mz&AH{cz@co zr*pelR#%<cXY{;ru3^r58!f;?{Bv77I89cfqD!%o$Txqn`ke;uVkqtH=4vMhXEmMP zMY=<Q*KS@kgkR%&qNLWHwo>6t&l4l<YqX^=H}aX@^;_KKYvhlbW}D@z0is5#MO(w) z6=aBFLI&~AC|tnOAKgFtaI@&l=Px}V5Is8Ng>a}%yY3*=Y_BO+@l`|EU(@C`$e-+V zo7`Pa3j}}EzgM+1|9FnG;R8b?y36eQX?BUw#ysC=yZs%IEvv#g6xp~!T|>ts2A?Z* z2TOpJ3eUwZ0*4>o6ccE#^-38T!+^I8x_)t$hyx8In9@y4-(_>Qjf4s>v|GK<e_!5^ zE@*=**_>IG=GGt;Rj*^|+>wT@d5yy*=AyEQ{N8`A-Xz;qvH%Y589_9k2P15ky>O0Z zf+94tQB1Yn@pE%EgFUNMhSDcOe{UKC6In@!W@y&(b<$$+DyB2Yg%_S<dl$4nfBv-i zy+X2sExH=*jAZom_L@8HJhAM@jv&!$V&r%_bW)^0ED58g$M6x7k%C#0s5+{o7Qw0E z%gKL9ekAB-Z?_mvIMte=MzU9ZsXlDQ1WDPUujRdK;cW}$I|tVb*_p*fuKJo*ECxOj zM6Qg)7isUgdQUlMXGO0Gqc@F-rq^k}7lhFtt(QVjSLfz|AtomriN|cWb$Tm>J{}22 z7SuFkh#o3eOY%!yePwpqCt1)hRI{i&xQ&1Q)y>}cuat;e+(XmW+S#;5KiPw|L4~s+ zVjb(r1~>`J7NlW@Vk_0v143?_a=frHT(|3R^s5U3@ZOl=HC*ueC35xpPlRB*PI;dw zrk{q_z$HI<0<QA6Bp0f!HJ8-YBrK=!{GJ!ig*dC0g|8(+!2SqrGP1_f10DjL6L5c8 zh`ChyF3+9e0u%MCNuK7GtTf-q;qsuhOch}2M457=rYW;jW7tL}KN<fu5@Kj+jB4bG z*?CbekYy_-!xNzGO5Jsi8Gp9iqNV2>V&H$&n%*YQwraU1D$pw0ZkYX!aV-+E+}=~j z^TQ-W{8D3_iO&)XLs(Y*ev0MFtJQz?_Zd{0Ps82#?M$s9E!vMz$FgD3&R<394GzRq z+RPHPZdnz2DYZnqRaNR`m)^Y9UT^sCd_ELoA=mH=m)l9+&3;IWqrh9>xSIFWI-Z1W zuO?PBx+&Hm<|g}l#woa{1k4Fit{sf7(HF~iExHbUHPjCb$%7b;n)WkYFKK^#IkGBB z)>N2p_Mp=&lYiGBMlvLwknH^o(frSxr{U#&J}S=ncz1Sk+>Y8lJbYyQm3OFGAV3_8 zZ~L2^%8wKtSA)q*Z7Up>T4$ax>jaMeA3eqkUZpYt48re<gt7UeH+|C>rvSqC`LFf* zM1g~?$u(5_v%nN-nO(Y@fRBHM=&=-SpmDpeS~Np?MP0zc(L#5`JT3b0%%JRW2_Oq` zSg81Kk&2!E;nJ*<x1;&(Kk%Roh^p(i4Px*p+<*W^U?paN0gH*?$L;-3+<xfF%o&z5 zQPy!Z;@Ij<m6~^QB3^_1Rx#H_7#Qn{rLMS_1e2unUEllec+tC^5~F`6G_T)zfalP$ zC}QS4@w@V_-ti(Q&@>0}fA~mDvVde1W%3m$xc7sTUSN~WP#pBw*hNy>OmwYe+WpQu z<ImBIy#lb#+4OZltufQX-G=hjS^4nluwz;~bYA|`I2Gj3*gW6kJprp~03&`s@-TWo z6FK`PAj1rhI>3e#>JxvYv_D&m8}Ab;R3OHQPgz9rF73<PuQ}Ha#e(>kpDdE&gJa6t zeCT4_`!Rx0rtVPj2_g~~nPW#3;bOXmmT)ksw7;JiW>i^wHnBbUkbmK4Q#Yz^rg`T* zp;#IP#w|S$7>IP~-CRd!aK>6<#11W;GKi)z)!5c05lZ>?>*jwWBOF6EJq>c{RDV8M zbzLL!(n#SXL#;{t_7BN=YM*B=Shq;N_B!pQ{(=`dr_jCG56Y^jIFAirjFUljXpwXn zA(!W{Fl=Q!s+%~=(ay(bvM&~1Z6HuCIlmv%ZtE6B6mk_QB1@pc5hEkGh#}Rpwkztw z{uMb&8eY0GIBkE=OZ&1@%W?Em%o(+%$DG6hEDN!L8ngcq#)qzTURtxZXDRyI9=Amw zGiE;_R5Uq+9Ek!FwEyNqREm1T7Fw6gT!t);fM@2rBvz|&JQ2x5=7!pS969)30X24? z%gZ(_Jg@ij>%63|I%Z51OGc^SKaYxPq$kKsg^m^(6vlr$X~hq8yLeLE2xgz3(B<7% ztZNlEC1T{RWq^a)jVnTBGnhx1hF9CI*Eh^R(z4}@;1!Q@dri%`zaw)|HEg{bCbUP~ zB}b>fNh?$HymN$>43_+O{UL&^Wfzm+L394kQdukaxzjwgn_bdmo}BTYO&-e>wG@?@ z39Z@B#EE}PpLS`!I<|r#=EA!4ZFEd3(HL9>vxoUIS~tDPR()xCP~5ozj)F92yPe)X z^XMG?QCc^aYP@xm<-HH7<^tWe&xmz1R6j`5&0AWv!V&l{Y!mj!`e1tr&$W_U>s9O- zpSkI#BH^P#C2_~uSl&$3gLyB(Dot(^hB;a_g?4{JICXmsfA#a8JGrVa1}8t~E_)yH zue73Q-)1S_m$CzRL)~H=hO8D?NynK~-4ssM7o4Wm(;ZNkd@oqM?q)3_YP}Q1e_eZu zoc5GK%GAT!$@oPtRNdcqb0vIrGq}YoET*3-oE3w`v>V#zoW^<2Hr7~_)GO*&x)fiW z=D&X#>@<gqa2~&I4&$L)>g&6%nYNAzi8IV_TEB;V(kRuD%Q5=#X2l=K?8?;Sz)6Ya z?p&0sT7>W>ws((kXqkpP-#aEGyASWU^_I~gxyq*JxOv+(oymia=wa9CVB|z}c&(S7 z#<j1qONy#a&;3PwP3hIa<a%ZAwLctCwV!`Oue}cl6}d~y?M$z*%v<}4Kbb$Y0htVd zAp4vn%gCz5DTZtlc`oCLQ9^hkgsz8<ckVsTL#>N>J>^te;`_}bFIO<w#B~s;hH5c! z-ve+Q{<${$_x~Swz!!kQKT}hSz2&H>Tm#pg*7hC@oY{I9>4X%0nl6-gxHIm0-+q5_ zTTs|>@;q6>_=xx^v%B>@@p?Ej;aD&w8`30|uI-YwIrySRriT&7o`MX~f4p;zwgmm! z6ICmVnXs*{Qp-+8ZSSev!kApp?DD9OHI!y}m&MYqzfg_Ve;KBlm=BjEa7`eZDfM2y zQuB;LHsRY!u9w$OKy<5emwI632y1^SXSsg=<I-4=N&z-yxCqI-;-8@7S#8=)WV;Kg zs{j4J0HlcMGSfd^Uw8fin}4;iplslKb3w@J7BDbq(vbx52_*Y$nNp}(YjmJVU_+|8 zU2?F<uc6tO*_W`nhuZWtMP^EXn-9*vJV%!GpbZ(hsxK_cVzcvprozS3(R+VqfRofZ zhuio4(jpDPfdENBw!g5jmXT`J$9_xa->lAMg+ZUeOnp+34_`|;VTHyk9VQzVVrGW+ zRMscflQ^9ZVEtaC8H%;7on$QW6KKSmqS`y1$6iq4CWt$uuOP_E=kBnYGxt=h2FdfS zC82@loLNya0-0B1O(~W)aHxUFKs>vD=x~z1+c;4Axr^7F*l_K^sNLyb{f)8G5Ii^e z&YY5i54v|nrW)IC`j0EfKGIyJk|*Iv7Zl#XUO|im8vU5E2U-t|3N}O^eXq{MELlId zo30~PmZc3S<g{`2ibOf>;}6+a;Q{V__Hh+ugB!1|9%t(@TR!dGK0ZQzysssHS#63I zd@U}BqIfI@kN$Gl0y=3?IjL#uTe)0yWBqsl+lI~ifPJBg5D)!EQPElzG*oy+c&>mK zFNI#iuz&psSTt^b;Acr%wE8uxy+JXo_-i&{m@RsE-|BS8l%IvMJ)(xDR9r_CmXP__ zFqT6p%|A(SHmvDof>>fMknn+jAQa~ht@+&h#GkORE#Ko`IJ3KC&Rd-viJ?2v$`lc! z0djgf>-#9&jw-`rk-3FhKG|822GitJRIHqF24eHBI*s*+0)GC^_Y*a8;TIPT)0GPm zb9IMRXZ}ESTI@4%QP83xApVOOgXQth1i{B8aKuHu^u@v%^k8vnK~n&KpRI|`d?yiN z#jE$n(d<_VbaDb1Xvgg&JF=Y-&tfx%W;vr8#zj3uJAy2Yvule_XFg6{O3rF^_d11* zR8`NnLd@6GDzcF;$JX;;(Hc_lYf{Ix_gz;xXaEQvNb>yV-e>5Z_8WcMY(?DGqPK4` z(MsuN;2<toXt?tyXy+7v%W`7UEoIW&KR)r2fek{2>5*_fHb4)WGK{(y`p9!J(V*&a zXZ|Qjkmi-|Be2-avj70htP`YHL{5#%lUMkTo8D~amSF^GyCu2Xs<*0b=M~217{7lB zk0$uy|1^H5aIghO`Rn)8$23BKW!oT>%7ISUP?qe39pvNe#?O9#UhFXeu7^<8EC&sI z(|d^Synq_&GS_GI<BVb55hGemnUce9lZ_}qXAZ1Jv@>Ao3P(n_YRwt?J1=|g%~e11 z)pXb*LT<c-BUGPl6EI+=i;MeP(VVIT8z?Wd`6E3(sql2as)MJJQt#;TUIQ52C?bG2 zCwk!FT#WU)P}W(0by{@}FeQvmgKd$FEG4rtMBpl8b~U&RP-Gat92v~HhFVnGPi1+G zyM{iTuD4cIbBxsumJ4MqOoa#n0x#jm;D<8oZeQ)v_@KHbV~=0!5igdDAAV=$gSFyO zCe^kSzh4T1CZ`s?Ci(UP?*G$FRDWxStY7GK_Nu4$!^iA@g%>+$lroug442+ZEyv_F zh5qalC{aXK+x$IGf)pp$rx*S$@J(IER7SqymP>u#%hbdRpbDFOU7^NjV1D6gTXpAv zXWWYNRza<Yv1)el?S(g<E_<Gj1}U31xVZTE-X@t4T2u#->nZ5))iRi2Pig_G+HCqe zvB5K!M{p5;=P5sUc|RONH4RcpEa(+Uv(A#0%Kur*AhbWi>e-};UlTImByqJ1l~D)A zoAns$FG@U@N9$mxXTmY1Hpm@jbJnP4nzZ9)A8i+0>dp_5oXlyV_$avl_i*X%Z|vrc z&h}6>{r@k|{I5nK8m<Ux9)0mzT=Ac0PtBm-w-vyD*%7&B$aC+Czx=M}96*l$@1t*m zXSgI+lXtfvIAs5h`g%g~X;{M5o}gz&9l1kz?ZfuSpBea^LC#ntSE}NiWu(uH%If=M z&tP-38R>Hd5h7?i4~VlP`9(y$3oaUas)7lt%hCV0o6O$8@n5U#@ZnVDTa%Gt<9{S{ z5#afMuSYU|M)grNw)X~|s1hpmu_#JOAzg*3b%_}-S|J|$Sa~FXns4fSYSRDhK|%kw z>VA;==?5+=!uxd(XmtA_N_`sPf6ZN`<-i3xQ)*-}UsbfWwuXhSc-Asmx_GVmm;`aM z+8)(+F7BwpyUb&mwAY5Mu&ZgNJ$+|tdYgcM>7Uh+(qC@!o+|@}M-kB@YiqnL@wvbG zOi7tDNAlZKzG-Udx;6|Yo;ZJhkbW#^**gdQ(KD4hd8+7?2^Xh8L6K?%N7Y;}V-^<( zXV=QtNo8%XG1s=Bjh)mO8fqVRij&mPsc7%;sY*$coweb~h9Hhy#Qz(qPt9xc63c0S zh9a%%p(T7mLVCzF_}4ECY;0_E=u}bJcZDpm$O7gURkw{rad3Kmdr`QAfGueOvvczp zxTO6fOVfwoRNYi^+Am5A4^DjNJ&Y4aGcb>pC0|;a429q`GN-+R=$(k8hwSa@?jFyp z5-i862iOVnMJw^c9|-Dgo+8YdTMzYrakz5NqRkAlaEo6QZ3%8@@5bW0MB%fC_Bc47 zW}3r4;VF`*@;`w%vWFn_O7G<4g!^oO<v@Qr44ZbJ<TpZv=`az`F%Y4pjh>4Z7B~;I z6{sWCq72j(?TQQfTGgD9*L;|bl=hf(lW?MC%X@6uc;6Z4u|k0PdYtK^X-EEl=BT&v zpaFjht2c>75S9O8dCxS+&)RpJpPn``)0fw^a!sZMnu2sVs;9>z8@;5T1EXoao%Gmi zrjC#N04@XWTfKXNab$)J;lBYV*MhfLX}{jJQuXK0Cs8r60^wj3JPw;=p;!9#u19KV z{N59_j@$Y-BXMiVnIoXMDQ`-De)qHR@2wD>-T9mle~-=Phkm(wMo47X)ZwqLBv6xI zT^Z{!@ZcOr?9VhMV9t+`mbJ=6wn+){+Zax73L`HcfHIz^_q$=QXw48rbsx|Ik{yp| zLc6mNOZlq&pI9)XhYKFG4(ROcY;7Ox?H%olqjjD$&*MUDxjbCNXTJh}4sJWI#1!gl zU;_+qJ80{)dKR=1I!`N!Ay8lQAP?+B_TMd9+Cr!?2S}owE@~hL1d)8l;eauElPE}l z#Q0$uJg{3I&Q9?}3Wb7Rx=j-k`N~$odQGgNV(-i`dPl*=K-!Z1K=j3b5a8@<EG#S) zi*RhTayWhdv|*HQDLyxUyREEK3wo>ZhswVOB#P}ka}LK5W9Mg0lM_T9cVKhiR6I_* zbVRC-J|LX_r6HLXX(_#D3<m}5+sij|+@7f@bMq(`OSkhAK7tz8anUpsEMQr(mL7;^ zDehiY0?UQBZbYUuuY>D{VozV6)VaxmcNpaVP*M{6(7HVaCZ>OX%>4G|CPGGAPBn%T z!LqP0F&;$r5vJuhZ}=s=aqPDH^B$~x;uQZ@8&c0Wmqq>H6#Sr5x1G#GJS~rufa^!1 zE)g}d()%s#?yGY3qb}-V)qos&0;|TOqwFt@;Wxdl7LFJpBa<)yCw-#wS%FRXQHnTN zPGQ*-d3tBnwA^Wb_#Tw_4N$D>v;RJ4$*aDLItcQKoV-1XELgsk=>OyqWGpviXV{!T zu(k1H&C<<!NqzgFDboL%jvU<7Cx~q`TWi6OPP{-iryT`PX*IR@DTp5N0)8E1O=+7{ zYZU>|w;bRuK%lhnO~jeED409qK#f#&N1COtzX@9EGy&LuDc+ls?Gj=s^42CSQv<Nx zLIEVKZW_&$X%RV{7YYK?4vlA>CXsBgmq$^uqu8qOO>$yzQE&b;f8L;#P(h-@S%#(Q z8dFNOs%;Ej>8W{71;8fhPkITB@4?p?o2(;Xli4EoHjDUQDzbDdyj>-s_>*4Xql{8X z^tt*QmO(9lZ-tcLcdFJ)N`tT(vycDDy^)MxOa?w+dYz)Ca64>-6iObs?$0RNbbuxc zCMHxWExWUYBbSA##Umr`3Pc4wm<2}niyEE4wFjkO9^qkr7636yohazS+7e{AxYqOi zpKQj4S+WKD8{2Ay>IMSWCs0UpaqQ?@FG@;E)wv;mM0g7t!DDc)5>-ld3%Q;)u5-m{ z$GKXX+2`sK3ia>KkM^B`-_%9Xb^jA|3RL<CEkoQ;I_u>o9VAqu*!hDhaewEXkyaEB z=iRX!Iws546JMXQGyTX~a9S3r^zzhl>WpFPrMj8G!O0M5Y2_!s?0<<#k%XULms~93 zPffyq^y`W6r(~hY<T5r{y~qHWC$A;jbZ7<8^8XtFPi~|WJkLd0DgNNtOpg3q+Ahak zF^?W>3O6J9S4F7kZ<l)CxIKdAoSc%P`T^rs&Qbt%b?rPaV+-w<Vm@re9F_kF1;JOZ z_u0Qf9?KsL4mhhg)IE>@p7-uEjd;`cDt7aKp@sVk4a(lZ=NGv4ZcT@`?bA00j;OTP zB5x^e{gJ<%f?GrL|ASwi8ayTa>f3LJ>bKnvevIHIf+NLymv+5MW>cM+Jx52>Uh@}# z!wlg*`ggrs_qy`D4}RY>Du*yHMuZ;~8II$UlC<&*+uDS%Sp@0e%T53vuP3jzCjM1_ zTo<2?9}c4{nm^5#iVU9^HrCt|rm&kB&-!5Yc&tQitC(ey=|X(ZO2ChQJgH&vD2$WD z561;4pxi^ZF2Wn=Fw$<yqq~?prS<vaqDzuTnrHOG6?t{4+lD9K`4wZVI_dD$6Qc~~ z5scFMUS&1$FHFU5sOFUlOzYY32`d7Bi}Z_r{!OJAtdsvPQ~PT#S7Ylq1jw*Fz@V3@ zQ?v_lT*JR~sP>k)&PAdSh4DJ+;dsdqTdqm%?BOPicY7&s+eRWTs%V_}@IFsB<+#G< z=63#KaS?p6ooB0<tlC$4l(Y7_T&d7?pVu{km#W3&{aIMp*kv{Gc-e38#hs&nvP`10 z$4b?91uN{GtV{)SIcx3{?7Aj$8UE2Ie_Z_tY<1&+9^Tz?NAz^aJ{mt6m(fye|7N&p zu2%T+Fxd~5;Hizu$vDvO7EonJ)C8qJ`MG=0N2ICoIR)b}745QEo6~`sY$xM{H(naj zH<wvqJr6>U7kbNyfZ9HmtmdhIL~Bg(>T1|o&dFaNrLwU-2fXAdgKU285(=x{dqd9x zEvIdV+bl!OJMtQ#DzVSHQ!RxfTy=d2^-{02NHy9ZI7wQB;yML-a)&0vuW*2;ROcke z*hGb^mtR)B+h>)S{}sUBTSOO!mGL#INw^uu>*W75kh8X7FU%{y!U#Kme^?lMbL!YU zz2KCF`0gBO=<Oo7lAq)EOd(c+Z!g}{?0o83quE*;XSuyZ#DS1m61`==zd4XQX$4?y zXqr)3;$sLGamURh;P7uI!$gia;?(pd@BlrO?Mf1YAljD22Yfmlm-Ph;?6;zw60wA& zp<~gH4xp;Xt<Q^>kpjPe<5^Eoba=ju8GkJ3eH8^=Zf=x4N|HP#ZtSw6p<fSstkKy; zeNZ-G@bWCBZfIUZO-wuy_x$elIK@zRmLKf=mSyRd3-81-ptz~|PGdAAF3n7}$(4w2 zlnjs8S4=F3aMk)a?9cy^CM14vEaH8OmX8Eg=MVOc7u^P~!HdIxDE}3|()yOIDBdym zjXph#g?=aA&YIndc=HA#DFuw|<TY5hfZg4Q&cM#FFvlo;Y>=Fbo(*Ky!~uTI*x3R( zK~Cn|Pd(}_ahrdDXGR6*zjoVylh;JOl>Q0x%0aUYp{cN9szEmnr+m=BGRN4;jPvEz zodu_sl-R$tf@Wra_#_2o7hbAFKUXN(!NE808AC@i8pWMHO)KXu7z;f&2Eo$@3UVBK zmsCCWi?C0BBk^6$p-$WS_@o+IXdw2Y3qHeV;!hc%D7pN?7V}D~c1=wu-k5JI7UAE) z1=G2ZpQ=l1kL@0hgA-@P;r-1-Hq+3lBl-eao3ZCgKi0B;zscbnE&(TvD8X2D0{ULs z4>0PohbZ{1h2jgzXRT5FHcw>PPN+zxm(*H!-&YQ7?IiG$Tej+(o+&-pS7K&nwot}1 z|KG3r;gF&!W94Y+HIrCrt$S2~f&v!@QU2RtNd(Q=9>!>X5BuCLD?S_%nAN=ss=#I4 z)U-Z4!0YjUs{`S3#)S>7XYte_ck$t(p+E3F0jC1?BZ6z$e`o0mg_mjaVSVARZ-+N+ zfC5TYBR<))|Mf`CPmT4opVqbYD`6iG6>O;80VeZfK4O{shkMJpcSZvn*Q-or=f&Sn zjW+`<iMC_ayG%;17r?dK#F3wf&n(|tzbVW+nwdX;;%Rz&9J(xk=;Ak<F8LJaKy2v! zP_s5_>bhbRhth$eCaNAb#r`u0NGuYBhAwrWPO)L4m`#CpPsOPJhzrH;#$ve3qJgJ- zSlHN`aJ)Cd)as|m0mvVIJ+o?q(&^ZNcA0TrdldJ~b^b1w1mMw>M1c4R-oJ==l3%!H zqOFX70=mXd8EQkR`HVtT*QfCvmV1K$OA(p-SIT71<@xb96=f^a{DZ0Wy0^6Mxc$oG zlFc-mz*o=pb;LH)X=OaC5T`w!tXi(EyD!UdaDRmkk?8!g2d+)cszCnNkAKPqp>KUU zT~%#`E3-ZU9BQ$Q9!j6tV0fL}09QnJGcKxsv*y(|S1lJUP{oi9$YQ&nN@A;Hm-l+t zJ4o~Feg~EruoXfUr=HbFcJL_enJq#|IXYPGYKHwx@0K@zXA4L+S#SWGB%^&ycn<cO zVS=>CKG+tkNk_CrBQn?6W5T+&-*cDT@`IRny+huDpRuAJ_jTmU%Db%mR&?Vdo`Uax zQoCduMHP|npW8~I*GZ2<XkK%=F5-wssA~|vXkSCPp(`(%t7Hz1_gNeBc_!Ma;H)iv z?HEg8ViM=M`1yt|#&Q4g$NHJoo*J|Ji3{f$G`y5fkBfArnLrwD%zSS$1UZ$aHTvXx z-0%!g<uv`&I8MP1@^urA9U_rokcPZ}K6>5g1sN<gN_3uoUF4UGV+IsX9Z1%zUM%jH zS8r4};=c}Kzf8@3ygOOJWQc@&#vFJ&vA@%Pm+1*IX}--LLm?^`ou@h<?*zzBhII7c zA?FSI$|uS0{Z2H^x1XgDd4^H{2>7YF%x8&*>!xA8^U{bAb=SB#?nJEw2jx<KCZt(R zZOPSHNcr|EE46D=2tUWjPa(~A+Fvefc}6yo3rnaw>dwM8>KJc!(<)aZo_c`Gm#;kY zIT-6NXJK8f(qc1S4nD2>a5ITTP4E0naI!r`19`I)^&eN!y6@4og|g?2)<eN_5H~{Y zhlC|<ABk?5H{=?MGBJ_p^c)U<BIy*8X5`5^ie46k%a4uJFjlfdY3~n8A$>H*&WhVR zWuVn5j~uy?j#N=@3k;F%$~Z<2IX(=-c1ly8=OO_UxuSRlw8pv~WX{bFRn8&)T(&#k z;XK_5(96xnlW_XLDr6ex@|)TK#O@YsVWo6SbtqII{*0eqQ!DuYkgq9!q_^}OnyTD- z6M4_(qu{IKJx<@+wY|Rxfjdi!MO#pdiQ6Fs?fH?iNlHFTN4Xegl#V{`7mFPI=erBA z$}XK$kAB=E^DSCi%Sh};!>`YLnlm3>Q}P%rQ9Fx%+KDov07Z``mEOz07uGEednOgz zF~6z!wE{2)xxZC4SOHys9`RZo(Qzes#2Gy|cnN}LZ?c{}-a=j^`%%vr3**peejchC z@tITiV$$?(z4exlIMh7)BpLm=<)Tg!q<!NfNzRy3Tq!@Ss<vcPq;2&Ocy2t*Wcumz z2HP{V@bpr!Aoz_>@1(du<E@FcR<#h6*>Uqyis^*Vsqdqyg@WOKM(=160m5_3$@Mq? zl%euy_L2{?J@$P(hI>W^tWZ3G5OXOlJEh0qxvg1FWVAa67X#YoxKNTr@EA(5BZBhs znSon0zO8`Dk+q&9NIAGCbwvG@+TrzcQyzL!N69)f&n_3nYqUG=H{3skpBWX+4u|Aw zb6=@b4dPH1y`xZnoEOIWTv|mlLxC&a8VSJ}4_Y8Tj|RmTkT7fhq$CoB@#w+s&D6uq z`QQKNR@7fsPkPcnbLVbe0y++p@1+jsb!oKogVVG!ik1xU0t~IOB*UK(O^GiIlI9fZ zF=SW3tp0flr<DM*@0I8j^v|fmKX3sydmSl708<-<Y9<hW*izylA|m1kpzv8WR{2+r z!32w!yl<=z@Hr(>W%e}~U@d;H`Wvfs(a^vmZU1ih3Z{=f)i{%3+*-uEu&OE;yC0+< zahirY^jY!t_ZI~E0c3W%Nko=Okc(GFFtL`^HUaAa)xEKV6}=Sc=jTvo^aky&AkgXV zSo&h+Skst)<k?O!Z!65l=JE-i!OaUlL@bi4$!|4Sk<UrziYIU+PNmH~Yq7$U#jn<s zSf7<CZ_(~7G<_|e<0~`4Cp^9nksf^xj<U3Z$6)jOe8T5M^vl0Ld5|!OKC?mcnziU@ ziE=OUT>k$uNtB1{+1pB<`Vthb-hcPCp6vP!SKL*9ShzX8Ik^WjwP;02BimGU>cd*( zAg|pd#u0x`lVv|uo&~bqTSZKM5Op2jWyXv8TL#66_|BWEbasRvIL|qi>B*^+LiHq$ zd2>$0ITSqdsFdCAl|EWN^it2m$>{ChQl^5usN)j%Y%%9S$H=(_pe86LCN>-Xqtb3( z_*cn)f3pdzr5}p#2NmG$m3y8Pk<0&#tq*>FuaMTBVEM!@1@ApWRg?^yX2Jm<Zx>F3 z?g)0y#j1SJoaS_sEF|%DUo0Q9Q@0b{z>If9qcSRykNIr~O<&x5onEqdd!AkvIUAoJ z^)5F1Wmz)ZTUGiaJY}(!s&37mZfaDejttd*xec}isVZ|YwNh5h{A4ElbE5q<Ywip3 zW{WIB->_oBm|TuDudbm<^A6U?NQmsq>}RM6%}nNLZ7U~;+#cl5SGb63v5P*OTjYy7 zJmXw@gxAXxo%29AJ?CC=qUWF~hW&TTIhYqxq6p<*?ejX~^=JbsJcEaCExoiY0WfNR z+hz6`@-rIh+YppZkG|?|tv`#NFxq7gH^IQpgZGO&ar4$TEcEvP8$jIkfShY8N<!Tt z_E=!kz_7DfQd|pE(TrB;tVH)~%jhgvIM){yTSe`>j@CA&ek5BUlOXt9&3frO`Ic8N zc!ZkhcA+mn(|-|Ric5h>FYxlL4DV%s^gVyU&i(@RYIt9D@hgK{vb$>~XZ~%+^eSuQ z<vq2E?h|jiP1B?4!~M2Cw5Vwb$Ku~G`y8k@iR*7@(_**5(yfac^k&759(BG-(Ax__ zu~Nl<Mss0$1=G_ho>t3=JLA$57sBtk;8w}Hgi=pbZ`~7IarHkgt~$=`0r<mzEti=t z;@hwJHWsL{Y3U!!D<->#q-!j{vQ4{i`QkW6x~LU9KtsK7rjNbpx`z$NVOx&u&0%Tz z2WgYE-Z;TLrUkrGE@UIda0{0+#^*)pYtLn+-?5Ei(G6wVOFI&AZfj3P4U}!p!|t1B zu&$4ZpUcJnRL=VNfArho?u}M|4@sud%{g|dg$?~72iU=0;O&M&g!zF1tI=QGn0dV% zOX(*mWd7Gum&Id7wnaa9>WZ{e$*$|t@{x-1>7nm`o^;lGI9_~}ZM<+HGDE|1)U|gR znoN>!E_w%P<+$g!ntNa8AmOPjn1R#CrA&KyIn4)?2dr@Adp0G3L7>BbUz0qcTcjA( zdNa-1z~Je@(eiND{$vJ+YT410=ETCbBa<ZFTbaiywtKB?qHXP5Ex9Ar_~CkK2tOY{ z!^Eb^>`XXeep-uT!9M?&43eh{!zlTYjY8UIj-9SmwR-ioHw3yI%QL2*$2dVBwzDOt z_ila?ZcI=U%8*7?<uWdRdHED`5z8ROQ|yRW>t$tZ9^wzX@aq-yR#Ojfp%q)NM*Tea zb#^<GH+eKX@tu(WZ0Vea8yI$&^wY|bONac`w8|Np@%8`OXwkjieMtbhtxbfjK{b<b zfqfS=trPYDbHWFo6N~G@gc~c+aKp6sR^#E=jHS)QMC$6im~Rk&X3tdN_L7=MM!&Z7 zAY+>~(oU70tSTVT{c-Zd<G?8e)s^r~RV>u`N2U!e^-{HYT0_?C?ltLT{lKtE^ZBEd zTHWOCT%W1^YmHaML7u2w7op>(_N`l;H{IXg)d&8Z61NYHby^*Ez7H8eHQKiP>OAeT zJ-lXPARvJEBfk@W*E=(<lg*u&=K&>`mGAZr#BJ;|5_KFKaJN82@*bu^aJYRh$lpXh za`$(?vzedET@!0r9;EH;|LZpKh488r-`&Dn>kE|ep9p&3!4TT>dSp);=(sO*nb>Ax z2YLhb2k|S~eBXbeg4XIKNS&>1=tH`PxuD{cGs3j|jVZ=|sk_2wPk-q6NY_&{4Kf2h zQSR>x+2@v+UUCcbaJ8`Xc`GsAF7IVJ$FBLRYH&IU%DV;P!>lpLS8M5YSAgZrO_{_J z)!ovWFK>HXMGnWYY$zQhoK>meboHA09KU67r<Fwm3kuXiUEVu1-~lb8-grgUZMh8Y ze5Us}G;gebK76;xc_p*xB8BSABv2if-3)oWHp^O|y28F=8k$ta)%I@mfU#1?zVT_i zn329=FISiJu7W1GR)+;wAbO_v#dSPnIo%H`$Hjig|9-SQW~6sda@4aL<?YbF!WUJw z+{h*#ti&+sj~n0kC`8NG;E}O@U6zyRwMa1R_hGw#D)L=`rm&?6AIZu&KauYgKju}~ z<ONIzLUAyr98Et`jnIqN*uRUHP~@8=K$Wd+4`z^l>+O$Z@nzZ|82a$p^k6FSj`U{G zJ7&PsH8|g>RIN6Kt!h8A&|q6TlZmf>{^+1W<8cBB>|(y>jzl=mp333!(JiMEKE-)7 zu8u5!zy3O6a1uGXF$2O9?lgqH4bxjJo>oe1cV4>YcCeW><(q~|HLBLXf5Kh<xNpJ4 z?t>;@()P|#8kLz@EetcQvl1F}BYSX1RB&x-fA=H&eC5X&dqSE!F_XYq6#XOM(-Wno zWz9}%&l3uS2k)=q=>#jUhzNv(eTjK|aj2bt9>m0v<VJ--l+rwd!w2Q@pL``|iSvt= z2a@^2<&PQ{PPRE{U-W+$bi{tjid%Cm)WY@Jp${z}XF4Ls`3OiOhK~E3(@0KvcY;Ka zh6m~{IK*2kHj7i|0jlME%hfpe;sxdjjZT&I5-qHh@a86bQnUQbOy9A`b=&Ld&sTbX zYHb88b@js4Z^^x6=AYn}<>jcQJqOr5tvo0uqPoJcX1GLM@{U6ky`fn!#8sjMe|<^4 zT_{_-=YAvMn@IF3?n9}35_0y0v~Th9GOHZZmVDIG7zT>%5{+K4ZlCT=f{AaLW<3o` zHN(J|4~qqq;BhyTwD+LG^<J*_N#yr`Zz}p6`q1kp742j<0sR>3_sw{YHyOTEPuu7i z_&7(8|421)k{r-q^jscH924kEHbHM)*YDJ5iJR$0@zU^IbXTLO%hEQu>6mp%n@WOt zJ`N$Vx(<=i&wHgSi4JY|KUkb`rM_C7Q_ELl4|vPj7>&&AS|AHkq&f!D^TiT>Y){@; zUk^?9kV7lGr$zb7L6%d=l$y=eJwvLOl+!15%>u*P<MoK&ry3`z$FgL3qT+xomV`1Z z{XiSff!U;DRiybpkKg3pPFFd6bXWY^lC$Kp(m&@l(1hr^JEJyq)Xc35k?ob6cJGEt z)K1wqGIT3Kai6dRRP{I?rLJdx(?7jBP0KeZE{d97Eo&RV^VW*Wiee;K7Ji!1b$LzU z+WB?A%EA%B`=Vnc@ovl3)b~$Fl2z|m-7fw0C5;$Zq(3OR5+D%n9~k6LoFw4Id5==X zQLmozYv4$4YOETRNq%W*8JO0c!XHjJ{PFCJ9~o{?BfTvL!o?9+RjKWNXVu8<=wBCE zyRcxm8q<&GsN=CE>H;mk>M6T6ZXoqzgt~b~%c!Oe269_%C8(bsa%l43INUpEH@q>^ zcv)uVyo+U8t}<Y1%c|!vJvbG%K(wV+B*Fp^)vfhPEz;YatxX!JA^BXkT-!Q-JOH_- zLBVQ);k^mq-nI^prF8>;?iLXJG;z|=x7WH?V`ttt_7f7_uo_WI$zn*2^Vr<wt6P8Q z-I|&ax&@VgaLk<9mNtJl^yc}*J^x5{ACAfjMS2X+mb?`7WkFImZ>ovP6<|4^qfN7D z<^p(G-{ed8=r_I1(@1yrLZ$vuFZQUJk~dz=R84<7)^-^h*O**?M6DI9nz;yuS@iuD z@orq<V`)1@)JfYwZo(zkLD`m8ytpoe`Q$jZZ0DC*ucAaB`1p;$O4+sM;f_=>TlVq5 zuUfm;W1Gcr2)=wR3=uYQ0x~*!x)<|%L9yKj=#_;VOol|y;$1~SoVu{}pzKd3C%7Ia zL;TLY(ttY2BqaiWgYR>GUiN!vdYOSoO&OC&MPP>tDj4hZU88E{;9R#-T+5yY@ty*R zg^hnSV#2}W5rKJn!^0aU;(oR-uUH9<T|xb=(u4a+E@K*<WqTlF#LK35`fDDhdcNf{ zVgX(6ofLjUqjNA{V*p$96H~oRGCAQp=Tk`<UK3oGW*&%tCE=H?os2kUlHRGD+AXiy z(SJ&eP&`7N>++e0o!78ybhCHM?QbS7$~hHuXJGcp*Y5qRt|i%}-shcn05+(8nZr1H z(sK9!>Hwwa*1g`!qsGEb0(Wl7n5n&SV*9YIpV%>>p9u}x9d>0xRkTT^o1b>0<(BUp zhutrt-)8TBmwQzM*iQ4DcI11P_-IPjNDbnLgoh?8bA&7N-nohU>Q!hYg<nGb2WKrX zIaFNl4Gkqm3cN^b@wcG)o$iS~^O2X@^1I;zV}&DL{#1$c)>Mfyw*&Mf<rgPQ23u2g zEy2#$Wp3EI>6vHTnES^wt&%Z$#%V4Is3R{8$~*^ud6m##<?Ys;6}dP4io2h&obw`) z1}`&LOC_Ee9%qlse(A(~Cw4f*OA*b{$zXlFk+$;{lY0CorH9*=X&-?@0l&`yvTu{Q z0NYu_`w7MYGa*Y2{xkE&$4T8q>oosMs-_8*^p*v(lQD<)td7~Tl}p515RcWHj(iCg zt=Qs!I$&K;VoFvf>fX`vkwRDT`&QW6hRF6=PMz13XY&}>7c-Hfxvhz%sk)-I+nG8B zkUD1jf0bA$Z?iBB5Pl;;FKLIX9*fZ(kGZ^!#I6a|IRMMs*!klvjuWH1b;Nu!u%$WO zWnWPK)lb|{zL(p3i_>`!lV-g{+b|mr$8WNK^{cpvCI$R=i&`FADYk5F3yM1HRe|OX zmWvHGL&%A~>CJ}??E>97KY25t7eN)9Hx?)CaUxVZWm?6fAr94jfu8_1QEpW}MYvlg z0UPTcgqf;&lbE|&zJa>lmy&gLsg8~N^YH^-1-1zM%GY$o%4rjpxo?j$=6rCptb1>N zEb>q5BWP2Gt4bCsFRg+tzaCjY4jwF>I)~R%VzO$*rc2M{cgkmq%fnzN6|@AtNa4i1 zI<O9#iRt+UJT7{v@@)bYVnFVa2h+LfUX4^*C&;DvdRv^3t0zdX5&5Ywbya#x_arTm zOujE3G|H|(%UW}iFyrR()YH5wg-wTlcOcgIWgAe(+()FMvWQkTSDVz2N-6q}LY1D; zAN}X^!-MpknyztW)@pv$uVV659{<Q)@-nh(n`*^P(3d1htBs^uUU)k_l&aNb`3Usq z@Fxex;in|WDA$`0<<V{uaY&-iSN~~QsQRsah@08MI6<|*)&Pp~ZJ0b4^{u3Tl>uF2 ztLHgOjY1ADk!lL{ceTQsM}HNG-i(r@ja9i%xC#+#YzGdw8#wDp?v7Xi&h=v$?NSq( z)p6bSn<X?c&2RHga)QD8GI=Sr_=Cc<O)3JIS2y-6t1nC#nzKW4;%+)V*VhtHE(qP_ zOxf_~W|&ZX)tr0TdBLgU&q(Kg$%tdsGfIs;42rt1<U0+gIn&twR=>uDW?FdwPOKa( zZ7p)l8o%Y_cmF|#0~w<_di_3fCbuY6BE`~%$3=7P7WEhOibK<ODLl)_q0(jA-dP~4 zoS|tKM@Ku+ASFS#<D`ywS$uuyh#ZRafa=BPBx+d54S$vZv$4sTqr4G+zrs;R=Q?9A zmYq+Yo7b!^N_s5Y+TS;V(^K|OtEKj8luOB8j!fPV)O1M!wL4RIj2Pb^HVLvrJ*;i) z*~leobdh8%VD}=oQ^f~8)0&DilGD*F!f-{MGMEaN34Nxe#5ti&+zr7`TxVE6ds|`W z?Dw(NTWa@fM_j!au9Z4}VAr(TU@JOAk3m;=vvP5FyY#a6bJ$gdNgA;%#T*@&Z*<u3 zHod@$SQ<JNcuiP%^hI{p@_+F1mQisv+ZJd-APF8U1SgQ-?hb+AZjIBpHPE<AAV_d` zcc+6pgb>`NaS0BMTjTJObMHO(jr(zay)oX;8okHvwW?NC%{AA5TpOMy9PQ?rZgZIU z+WYYjc++^i9%o3gRHMiL-Ovf}LEzX}{qyQl^l6d^{`RphGYVr+Ro1jyf48<_ChY1M z$LwoESmjM!Qv#g{vLG#cyO(0kuqsi-iRQ-`J&yIBQ%kYxx#w(;pfk13?uVjz29I3= zZ)>g_BULAeyNtnqZw>CzO2~QRb(v#6!2>9QT#AJfhl(9WA7ArwZ#A8H;tC3ZjrW!d z9b26ja3lGSSEJSi=_WCan_*8&@99I=qXdn?yS!U#C^#$Buu%FSSj-$V3_=yy&?U75 z@@LVSKDItHp(~>S*~tt9kd-Vt!M2#EA%Y#6O$Eqa$S<6KC!6$MK<8KVI&~TcY_`O| z{b=qUEEi^?H$DM*xaHJQjtD&8m*^Ej?k{9p;L&sFwiCQQloM?I(Tcg?G^0K_a1p;I z|Domd8e7Mvy}W>oFZ~5Fw4Sjxqg)4Jb-6dr`5>92?K=OlZLVJ+#M!!u+~u3TQ)wkp zT%|^qyTsXl@D`V#1gx^%^vIEZQ>u5*C&zt$9kgW`1<20ND_n7Kz6J$`o(DDsI#jre z=3M-eb}X0<oC)V1hxDa3440;NhtJAQ{&Z@fs8xMrXj|?{L5@6X8QQ(aM*x)V)j77? z&0yWqjso6R&b+sC)~pC_ud~v--<9abNYm#V1$MiC9Bb(+=d%KX|D>1qUbyp>)uTor z!syL;e11{xz-z6UlNuhrY25{G4R;&Y8-_!YnPJ^L?y3DV%G%Bfk2X@Fum~XZXq_di zJCj53;yh_<ba-pKHd4%aK_1U{#^He>@@Uuo>bgSRmEzS30wRUe#XTcjLu7?Ns{WZ# z>N|yh9|v$6s-7EJFf?o?zvks-ZP};!QrXhR8G*wk;r9_nqLj9HaVO0R2!M|Mb1iv` zHnR_zGk6-}!kXTup{NHm5ME|EJSKK+a$S;XUbA5xH6~z2WRP3}67fyGL+%9Q79_}Q zN`c?gB-oMS&n1-VWjN|LUpw3+XpQca;d~!|QMuMeKw)}Axk6-F30+-|p&Bt<j?0<x z23wK=Tx~U72ZFb1Ky|0nwKngEP3o$@oZUhk_&~B3GoWoRsLiH_UWU>Ve5Mdw2I*X# zCxr9q0{%c<Gv!ARhPMMk0#7D0RBxdx<ArXXVm39MNrm~~pYwdHW{%KG3A)|jY|G<+ z{gywI9WQlM7BTg&=5A{Or}o8DROm9&{FG-1?o%TEFCy$%<X2ERK7X^Ps^L=|f8Kk; zUGVuSiqDKf<hU%w332$TGD`7ZHU0%yCgdric=+>|-x}<@uT%Y=FiGk0udrn$9{VZc zIyK~vL1q(J98Ws>@R#%Lp#AtH9g6dRw>==A(&FV)vvB|SZuC`^d8*)RLG_nOmzEqu z`)?ame(YWJ9l)oHk%Q|iq0}oj>OK)FamDYETIUF~uew#dTX>7!m*rUh+b(g`Ms24( zOM(C4v!j1RI)`yyhB<8}B8J+R93_V|AKraBeT@DMt#`JhI~3Lx!=aSV74_zSy9C+% zNv%{~S$cVq1MhuD`*k5`VBZYBV()u=pV;`FxfcDEie<(XzY#*={4Qw+HR6A}r_}M6 zH3urn{8te=yZg0?*G-VVPI_fueoU%;ztfUkx0*b0dUi$`{%*fJz-^ELiRfZ_&GZI# z+cv;V6R%;<=Q!{>g22%)DD6joQUkeSu!A5~j%16gndO4Q<LeLLPAr?jp@~ZW)4cx0 zrrU!9{_3#(wHH7K_#xMVYf_@A%`mjLrenf?b1#0KNzKQS)$a}<*csavIM6>*1Jv2H zUT)U0Eqh(#9c<YZ+16XG0bg_0Ibh`2D{RAqCs47jv<~zO=6b1q<95w|TB=MpFnv&a zfSc_`>&5t{Eo}5x0dMyNaRpqWEVJe`40uzz!>eobx051k&r;WG=5dQD?85OPOy+G# z6o1Sz4Vof-DA<vDblpq7cSe$_aXtIDp2-6GXQHg2OQW6tD!|kN{9@Z=&$lp}VDTQ2 z67IfZtxbE@C_r6TbVb{LqFP3vRT}{Yd{<WX;NaEt5Of`s-`}uYoDntya+I!Z&Jy~p zoYh*=#2=YtY`swRH8e7QDadu;Blqsarz)A-4fJ7!LG(g-sg(Q{GpCt-e4vK5{%1SS ziTJ(v#&dK}C#_#O&NtiadsD^}ikv4uZVQKzI-S~QahI;k`a|b`?@At;>|E_y>ej;c zid_OFaiua?tWytD=Yf`}cLxz%RTPT577YQ0szdD?)ptm{9`FKYbt9yB83eI4K3KI5 zWc=He?r_F+^E3%9=;%A`3~XT4=Au&;oWdnZFEo*d5NGoXuVIJH(?r+LtmCvEUYu9i zaYRT0ylY;TJL~&@bFO3F!*snLZsaQZ!X~D4d`mPP7ZW00($5gDtD@E;F6_ukjoUQH zapm+)bmQ3oTy_Gf@hv*EWK$fz`Us?BIX)+C|L-c7$lhCn4UN4+-$>Gbg=YfF#u4Xx zY@lnzQEJKPq+<fxF5A7AD5f|rsrq9~iA2F?QjOTc^ojm|w=kChL_Npu1VU4P-pUE# z3@zgb-O75n53m<k&sHKp>-{_WW2oa0qCCOk$Z~`Gfr3^i7k#lYl8tTaJz>WDcu^~( zY~5C$f=&GkAabf#x04>v7H)*1%v8!YW}JFEWYA8}wEnR#p7VGJnQtkar)77Gp0g4} zBu>1L%gjE1{JMH#T#>D_0JSKi$fpnsi|W|9yjMz3D5qm7Xwin~fOi1lfbo!nV|KYU zLbguG1To3E!#q5p?1N~LT(eTqWtBe3c<!jH#V9`6oF6m2o$Kgu+|=B@^zAuO%l^B) z-^1Uk5hf!$ODH+(qgEtf`BnYOE(B?pq|kc3hXD$IluOzggW&p<^~-JyrPFa1iNmE- z(^u`-92Y%lPTjO2p?wRvf$t++@Jx4xx9|KQ&$snte-FY?{w>bQR{QcBzX;*KPKgR7 z14*-b0+IysQ6~D=Tq44xbF%u5^qF4X9F1vs-Bq?}pt6njhi3p$AWph8><|-$*R3G= z$JFtE>N-as#<Pz*<!7qSH(Fi(v`cf62hQ{sX9MiuT~E)Acu69-ld>=UK!JcapJ)5D zCtpbv@^|OMmBCC$2ELqzpbr<SuW)?lt87}T>lvX#Ny{^#UmwQJuKje7<&kY(WmejY z1%bSLTd-sV&(P3?FVHTkLm_tfzkf`QvK|P3YT$Ti3ZtZm01dt*CDc2e)RAP<Qogbr z*ihYPw2hJsuD+kV+qTgv?*(d?cbO^vIuV>Fd}m~)EqI*>a=H*VFH`Khx_K{lGjD3* z)iHqqcb4I*2<6E?N_;d(ib>{BQx49En3$;EB$G@kiUOvT%$fbltTVA0sqWALqPW_B zs01~o)!1}MH``hV|8@cV?_DvpBJ=5guE%NWH#j!_LZlGv8<!Y4rQMY?d|KwEA!)pr zN_>6!h9KRN8qOjO+K7h6eJ){YMsO9LAfIi~W!-MM5FmFJ(es05RYoV7-f7)I1?@J~ zE7Ze;MKSN7mGWBVx1d!R$$T2T;9;qMB+Qm%7c#fU7$MU%$q71<V6UY6w4K(}kCHSx z8S1?WZcww-28YxMYTvhLE7!eq=BWC<^dK?ISk;agN#7gkp_Nxn{1$9k>gB6McOG0= z?=4|95e<KEXtgIaDQ7#Npg1tD)YLxD#{EbqHn*ELRB0hE<Q+ayf1rGW$$yZ4|26oM z<D}#0;38C_EpC2in7Kl4oKSET&6&~0TX*zBT+u#2ie6kf(f!mIZV+*aIa|Z^zp}du z{Vy@ViU-_h{}q^=C?7D%Oe7d=W=a<go89Dm@kj)6ZMOj3sEVwg-Z_t7|5XsZNj{L> zvVv2~CJ;Dy8dz%5eNYdCeiQkBCLMY-Iock<n!8hs;i2b{Ks>cIM69YBq@Tg7$lp>3 z+n-qYfC^8NSMps%bX-n<Ole;sn0;GP?0-M6%y(KQ8o}w4a^3$b^|n7)+b~Lm{mkk5 zRan9x__|eK+p(>A3`H}AMcXU=1`}om@jM#}xQ(&d6#TBE<CwQeMrMA0<nVw!rzkd5 zkqKYh%k~rWEKxkJOEIqin`q{ZWHgsG2=Y~aZgT$^8VOHTwrkmhy=$zPAiUrbztra& z!#x_$)!5$4cBKDbxk|0fAGA_Yy})bnU$MmWCdH|;@H~V~(dAAq?6O{YWwmW;q(*F| zh)k*DjCp3x``$0%YC{=+W{SX!l_@48E32WvvW92*ss<s_uSylZQyl$XWx7;QQghAX z-eJIi-Gx>W)P5&N#<)~WGFQlgLp~_zAVQDtlOMQyeJbuc6Nob`pf}!6IM+o7%?m#t zp4Xhx$Qu#>!WVT=JD^pS)7CnGIjZQlT7*p~(;&&v{rgVlPVWML9T^Nuy?N4G@qzqZ zLT!6r%j$ibwD<(`mp@Z@n<`W0B||8Sc4Mb1`7+bE{h<HzEr6r~aa7@%ZDAAGcWNiZ zRIw!&Ehcou(^n(&P6xIU;HIgLB45!PF7KDOk^_P4Y>q*C-d)!)Q-A#h4Q8l-<K1q{ zU4ziesHJd^FvWI%smYt(<ZQD<{oAqd<^Pug>pRll1fsw<$l9Hv-IdhFJ)*Gtzo8+e z-x5*##LuI8OJDsM-SHFHR9(_Ys#9hVRU+nEBkE9q%D8<fGR}w6>-X-PE;?AXQr!W# zTP~jaY1i}D<`I~0x1I4*4{W|#E^XE6I)f$W(~~$o({#Rn7B1yyeQ1bT2v`?QU4_iV z75WO?7o08>?kw|D4Q|3;?lFUsCCzFq!^9p8a+@o8h_ni{6sECNJNuXW@c$QrO8VhE zU`<!4as4;06$yK@V?*Wr*mSM2WVCduAsklbK;Dl_%#^$y8bZ(g$uivCn<8vR$Hd%K z=ghrno?r2QH`qnX)m0)H`LN;&&>}O=T2uAOU*Bc2{3l67dE06_0`GO?{{nEWKj_>P zr~cmxExIvxrP7ylz_0wga;JDaTRRGToV?P!_V*cIjs2Xw{#Perf%UiiOwk-7@nqYt z_!s1(y<CYW^#X6-|4Amqop2(alwAHzZ<>o^UOfeW`o&+rxwFDr`b_y$z5x51qcB-* zJSD{YV=ZEb_Gj*KsizET{ww1MBXs}gPtOsx1OC|ac3hqV`zeE%{^l?Lf4lj+RF>-W zsTA(7;3hCIP`ll$$xA4Uf`Y<nTFac4i>rQ~s`h6L@Or-8CoEE3*3lnXz|v#mND(B~ ziJUlpSzfaZ8o}|MX(2-0t4g?@Z!rz4*Lkcy-FJv089D91u;yOD-1121I3e=BI{@%U zc&-QEOs=1m9?cXUCiycIfag@B^bJG##@88UD{2$UYS$LjWk%@^fM7qDmp9~lcFCc0 z;r}-Y>w5iJLIT!u)IM3RRdYVVe`K-J?#)?$S`3Z28_>FL*T1{_666M+RFrIhV5ux& zyKg2I*=p%1F*Q(&W@3Zy>l#_~n0<~@s^YwCRL{o*lW5Osv^)eSH7X<P?~8U}>$2zS zfNH96vixt@4Xv4}=_BY+W?OIWOGoF9W21{O+L<W_X27VA?9qWu9?18e2tA6qp)3=B zICam1>3v)JP-W7wM7pQ6c%$1Ush_hz?aBH0wQ1h8{u;Mr&&M11f=o;>PKo&c_!Rcm zu1Oa*mLpaXl|wb|uZ=(N#ol;v(J;+?fi-_V+(1VvsHN=J<01d7mwZ4kn-a;k`<E-g zx|=*`rXml?j3$X*`Cy%XEX7;W0d*sPG=%$BluaTH1;xSn*JsChX9b2bW+h15Xj$@S zRbU{+3+eqMv#H@2-3HnY(Ir2ZG>1qJJ*zAW3Oa>_yTF~DRbBT@70Jm&hcLVjwhOXB z&Rf0lPGg=#Pfc4@+m_Fh@X~pFh9RPMjJU$t_#|)wKDaZv8&9=;QK~FfO3yQY3AsFg zrGA%P-nWShI)Z(-b^uv%&oxE2bK<XKOiz&pV^kg98;!)|AxVrUBi=HA>x33-KP9De zir?!}DVb-;;^DIsg8sHp4VufkT3m_W52_&El*f#Ah&%>mVjeUX?4*ZmTfr=)U2fU% zh01&@^a@|#-^+D)7<_9bTA9gz+B%>C@q_-6*?$+2mJWHifcRS8-(1{4AwC6xsQ9He z3-z3(&{ghp!c9<02%HI8Et<sCY}Y8HQ!BATmRWw1Q6Xjz8ktnIQg>>VIUTTFB@#UG z&~w&StXV1ht2-b&WJDo#z1*;D-jnfVp)$Ys61*6f@T`qZ$c`XLI8PdXEZ0;C5%S%X zZc8~Jdu|qynBoAnNo64)VKQI$$3MRbN;gO;(sQd}B3q)p;`<&89`OAYPxcN7_1+0= z-iuX^bp|cqpiZ%ulyRAno=5ducdL1y|GYb0D97SkcJ?^A4xHE5E>;X_KD|lftk=kI z)fr~|T7zj}GLv_+`rF=r@o4`U`x*?l?QE2HUIp7C&}lv#xOZy!P@iOjt~H1b435tW zWCu=QtNQF<gNc2Cuy@T%W9ls?Bi^5wTc}~9Gm+)-T;(a!mKTGI_*qkB?tVV3Z@G!W zMq*re4R5>C3b-@fBrV_D5P6>rH`AGBk-FQaWVjy6Mp-H(D=P<oCwf=3wfo$RTOxUY zp=>0qosdYE%0iw;49m_i=DofB*DXlX*LE4K?`+YGUZ&@iyx1RxCkc7CNQ2~+YB@w; z7v{9GKM4Qtd4<jp9T%5J!tb*4Xa7(A**BbKL&=0Qv!(t(qNK`%d5jtNJZ7a4b5Nbz z99KjK$DD!t!H-yfZsq~R03RC!(cU_bX}PxB>lQ+;eI`dxlI#nKs7dGT2sNt?o`c(c zb-^YW4tthHX%HwQlhdzcC;!Z86t76;l?$ElJW6uGLdN%*B}LZ<k7f^v?b*(tK*z+L za?irjUqi~KNbMuZ_BCc`d-;s0nb09|Flgi30w+tYhTNBbL6mZ5#kS84ieyGxO{tKM z5uJQ@${;HVX5W@cwlJqnMO$^N?Rv8hD$QG|se?9QqvN-akJJNF?+W?B$vK%ZEp^8g z+~*IVBg@}l-*QcG2mxwRnR8}N^VxI=><vkUzbSiy{)FtMfL%s&%J=1wh!iUg!rB_l z@(Fy^-mukwJ&UZSAq+1)hkI{AFoG?SxnSi?{pEO@f*%CMREHQ#KPt3hC+uPrF)KIM z*lDQzwtUqJFNtpWE+oqBb~mzxNtO9vI$a~Yd&phpUO0(1cfe=2aNJyR>K9ht6#H`7 zCPXmD9kqSpd_}(yD6a!~)cU@FIDR-i`x&&M(Jirm3F=R!4CVG4RAr_a^H)bb-%J$M z%QQm5A$q(UOuubTG<6*xwY^UX`alZx-+n0IUcM8>T;VMd?OM{ln&YB{<2qM3$>GJG z{@>fw&*SZP39q)xWzSRr_i9p~E5kZV)!W@YP*z?hX6{Mo;8;?M(8aq{BMn}Lj{u~! z;R;=Uzn96$GES$6I9JvFSO*+i1E^EWLvt7RE|2+pdJ^e5&nvrRRvM+}P7V;7HkR9! zCGjd`A@7RdTYv_5+sk5e^T&ISX^|8xd|eBKk5pE_VY&9SX6aD?Pe8E0ML$U2*E>!E za2lo6m|KPzPcLAW2uesY3%g_yxWzx(?q6-D;R||7e_FMa+-8@x+$7IfTDBlabL{*C zanu9-jl^sl7#fD<sxtxDIpZ!ma`nl}sSdjqXC?s*rW-HA#uESrLyKGlM8>wV`_TSc z^Qh&t`zClQhj%c#dT;c?n?Z+!N_ExXnZ?9iRdaL}KV}d?^aRw45joPVhCAri=unwq zu0hk+e`lZU<}6NecT`chAZdmSJ!$@28fpgU2v2dV^3duvW+;>=q#+qq3Tn!$gZYFg z!>*J=d0Us#U!}lpq5!`Kv~}SJsfCU29K9481k4yGFAFghNX$l_gP9}3;kzvbMEk99 zd{w#i3}2)h!!UyG+4iD&GcaqfgOcEFDpqiBf4JYHo&UjnwLzhv_UQzu2+(fIog9ky z5456T0N<ZGbrXPBM9L;IjIVYD@8MomYeTJi95x_YmWopq&NUZfe7?b-lUb&)&+>nk z1ziJ^+{!JdWs{7OB8*?8EL>-DHlS&*7AEiTIDe}}P%Chn<_0pekE&X>qpWx!ZPl|a zfAF85Dt<pwgjGxGFedXeCsR_fDU;k+<^1Mi@9EbEp3Ui2U|L6n161%mC+PUk*&21O zb+9U~#G7nf0%K$j)cHme)fK$JIBPnf%v1{!*(zi?XVsNx<XPh&{Sy8&{GGZ{jomCA z4cg{Or6q@ZG~XY613>$d!pP)&tgW{le}Nu{-IpAdMaXy7v&IsG^6bxh2pP<#W==?P z&a*U%Ql$@MzsjE+Bs6{0T`nCiFC^_}k6}t!CbHflq1X9&8O8}ajB$ThP+aK#*tg00 zys?keRI5yL;p%|O^JoGyb4JB;c#(fc%TfX}lQf;9opn^0UI^dbsZBqtjdd30e`0@+ zOBd60jMP^B9#{nQ0_`4jcIKD=0~}@}kon$KB{Jz*z4}qEL!zp3_caBkQbJN)EZdWq z&@1QEVIAzHr#N|3Nnq}e0eCZ5Ar5KHHw9L*bV4TgyFxe`CXkX|HwcOjdbI5=K<XwD zgAW^z%+2x>=e}PW3tnkkj{OMjfBw9$oOmsiA8VH196v&7ny`+!pb0WtYBq%oD+xM1 zqCE!L0owoublSo#k4z~i(wq2-_3k7k7$JC+W_a~;cu5xPkre2PjObUm`}LCX7YEh| zM$;VTLhQ=E*Avk7TFaY=>!glNRl$o+!nPLc@Wafh7SYm)Bq$Us%SwXZe><01-oaJ6 zwNTfqttVn<R~Hai0T^dW;-nb<ye-;XELvTi1Wnf3be4M^a-I^5hnRSLZ)E#;Yn~d) zpII2}%Q~*$W3d|NVIAT9d&{}1<6Eenp$+3)`E^RS4BrLalADW0Q^lF0mdD}swf5bx zEqd05@0CbirtcPQhD{t~e@sk+>UKeIb+2+%e;O4uY|}L9TR~8=`L*n+L+>S4-{|N} z_NTA=zSJt8_8JdZJ?j#2C3c38%}d%YI3yfFKs7mYJv$fS-5o2NzKHKfYG>B$ohgh| z*&BlS5Hs&m<ItIsuh=r)vO_ozL#?V!1Rjs~WJ6rO`SetqxVP}Wf6J8{-B-h^6j5dc z^nzv96L)qQacmyN6p`QwcGT%q2l@6cf}4kh3%@rqENSXKk~kHfo83`$lLGP+9(=(0 zh7~JCtyZdJu6>z1gNU#*%Znni<OO2+9)Gmql)9eG*>(*nG#ZW$*HKLEDym6aofiFj z6G5q|gUZ|qtG(Gge>5Ds-SLvRAaq!n%kuqvdyz-c7#Gy3jgP%-+y8CTu7`P~JDSLX zA)z%x=|R$KN7CdUrVnuN`Ls4pH_GDSEOU4GIQ=;Mz|(o?+<Ygt#vw2=Sx2`>PKs?? zZ}&QhM=mawqShD6y;VQgBlI}rZbMq2Rclkfl_Df$CTKXZe@NGCG|bd8mqK-Xv0vH+ z6FNtTura@}9DIAb^64x<_8)DGgkt2Joa+7k{ZuZilW)&FcS9HwdgwBw42L<vTh;0= z+2%jLL@zwZ85>Ka=lPMfm3%CN7q?uu3H5}(yt(_DRNJ>-lR4zJ6~4#$i_ntef#E{0 zMF}GGnD6~<f5{WBVx7YY8(<hOX?E;U5aa|WkotNgw9yJ(tK9oEw-Lg_=uMk_ob~<m zHLIn37Yv{luyB$b!*$8{ES>&%-y?N*5BO4nS$_0mk0hw_dsYM?S^fS1zY)h~;-J&< zHRi>{nc`fFcg&LWl1<WrA`t^wDebVH1%gRfeX{Sve@u-13#Y;SfxLinoW6iBLn_WB z`Z&0Y<|=q#y*%!E?cIv&b|L7Svw*=YK7rMV<5x*`)Rv70fGjn0E|yiLZ{pnd`-Ky{ z6{R~#HB|Yqy{PFOa8yOrLgGY28~l*T#%xg}Vy{*cIO?f4I4sq+cgTh*^IfteMLBro zP*v_Re~fO4kbgg2Ikb;Mkz|<dZG_juNBI`%B)ry2O#HSou*@xweP00~Hy_ZF2s7m? zn_rWB)^1js|B}HCjB8TkV3Y0ay5=88TJ*oZh1Z5hmQ!{FuId=LS$40@BtN|vS;<oG zKW}liu_9u3=%v(XB_ILhoEw2Q;!?=y23TQhf9h2Wgaqqsd7i{LYBY?;6E-k<R-8gf zjWkGO_7B8xia~j6GMa*(m0_AJ<ByySU5gl+)+8!6Q6(xQJ}W~p8hZ`+7noem*I~Xh zi}m=s9lm^Qjq;h!Hyw%$Q%y<0yqFBWQsZqFvB?1kwI)D83U9k{CieQSnq;HT$)#gF zf2U%y7`M(vzW^iqfaS#PcSf6^XQSzpy>czOhNF995-rYlihd|ujAI=Dk_qRIc4i)+ zmIdAP|K$TL6c8{v+PRCiIQ4ZHUXR*?(BdzqKAR^Nzw@L~OqPIh?cmW+^-SSwLT)_c z#Y8G$#2B8#Hm+3|X(ZfG8uGR18Wj?zf9}sWYTvoL<i4+nAL#0n%vZ~W)lg1dPL1H9 z*!@8oOgFz78PCzjj(l$OWE^AE`;;~Z>4t|+y7^q|XxVi;)Rsm8-p*R7`*-ZyP7$N8 z6r1(EymJIK(%p?j^)TWD{*l5pi_8M=iP}D@xnZLw>rg=6<q6gdSLBS|g3;@2f9XlM zV50Z=Ylt_~Hu;$OnZMpLUXA5S!dx)}!Y4qT!J?D@XAZQZo;3*8XSQ!iJ<&D{p#ISZ z0nBU?vpm6-v?9TeRv;K8d^8jkKOeTIv_V;Qnu-Bv43bf#>O~3}2+RjZh#dp=Pqsm3 z^P)5ki!He9&*m&H>bAItgoMb*f5@<nW`tE%Hz9VEZWvNOReM%NCFXaL+na$T&FrCb zZT7`bR!exGlL_foRjrb3Pg5m-{Re{juR3&&2mk<R`k|2>0$>@M*MR=)gOobW20u~z z_WyCR-ivoN6Ai7ioRo-(i80(BN2%?+S&o)<J}B07{SD|1U8tB*Kd6DTf1eJj7(79j zrAqxs{k}xlIW_j2dhfWLFElJ{P#&9%XMbxVQp8iVBPl*Upq-&XA1Xxuz7yi0+xk@7 znMQW=Z+WrWo)U#38+2t?i<Agu>Q0D~{Xc^EZwJvI@UP1WUsEfZtPir)FrJb=F3a(Z zIep?$Iz&RsXKGa*MSL0cf9gXTD-fbQ1%Keh5-9$O$sixI{@b&VR+;_&DQ&_9qsVbt zyErZ86NKSszt&a(o2BMDEYP_|LS|;AzrVi(q@f<K*(%NN{7I#Q*bfx1;Vj`54AG_~ z+lY6pxypp~9v4;>+I2Dq3k@^pyOYpLy%vd*2Brj-(<hlqKPqgRe~C;IPqnlY3D5d` z%guoVnFagrhc--7GlLzEDU}@$9(tE8?g@L$vlSewy#sZYe41%J&^B=tbT6%LH-StX znhF=Ku+Y$bv_DZ?_Z577eO*VNjit(dT833sg7ObX6-2Tv8uV``!RN{VB6+W4q<ad^ z`qZDNbywhqy#nXne*&7gTFp>_ZQJ`>_z85QtkrXGzXPN^f^>7XQk3kFi%S-p&jQNv zxxUp9t^d3m*)ck$)h5aXJ+tcmF+*FFo?B(yJMGB08b&1S)A}cl{=_5|5`w|7GvYhS zj-Qv5p8wogO|NmwXI=<DR=l+cz?M%d*917r+D}v9T*-{Je?7YCK}Nmw*vYO7WD<pP zFkO;qG?NR8$t8JtdGRD%@GN_S@<Q)?_cKsM)S&Rkjyzst?oX(#cy3@JnNLV9!k^^7 ztF(j^8gMHQO47pL_+HD5t1ygXd*y4EY?gV&=N1|u!^dRC$}B#reMwk5Vs?{n{5Wge z<MbzAXOCiMf8Qpjp;_bm=xCO8uX$TDGiRRRJU!>Ah`+eG9o%wgQA%giSrk&%Ujiqd zh7`F>LwagRe0%rKTt!17R7@)h^%#4T>?HJd83hE0pFe+It=00PoJ-<M)!Cj5=w(AM zV)8Q_0|dD*$SlvPe*Al&mhnS=nDyMZ$k1K1R(48Ve;hA^_90TV?NIDh8l<%I;BtNL zw{X?w85w8jeRz1jIm=oJ>pONcw9MQYq7{B!(QAgjWI^Jx2Z#{Ue)>nCr|!wpQR!*X zNf*Hj520)xzh{)qXk_SJB^+MB7Z72T?O5*!=Jx^Lu9^uWnfaeFzM2Jh+?xsO(hJR9 ztJ4She+B;2!ha3usK#EC99qgoGKcA?CHj<fa8N;JvbwLjV>Zq2>{>Gm&-Q~O*sI{j zD=hBI4g_tS-Wy9-28SO^9*sW#IxpB+x<Y_md`lYFtAyxq1szx5Wx4SE^5NH$qV=_} z)z08Is$jhJ(8<*YgO6b453w+nL2?ruh}d*tf5gmPseE2%^*>Ge*Fc@>n7*x)=8h4a zkcL?dWn1>^OL@D=m6kP(F5+-9LHw9Du-M2g0gq25sqKZuDg#DAmS~>O=&G%7w<X@} znSDSxVbuQG3f6qSpV#my)vzlgQO=-&Z+-{s+mP#+W8uq>Z0$Us7^jB(fenies1y_w zf6*~9?=$4Iw6v<Z{rvn$+nTYFo0^(<?0$c-9x!3caXnhLSk!Dy*7oFb@quPm8Aw05 z>@0qCI6Pq$iro|>FOy=oyVk1=ojhAlr**xm?^W-VtVKE+cl&(3-#*wl#x~(*Gv|<a zsY}b-nH$r$)A7BiwaJ&cTx_dZ5g2;Bf9;i8xuh(2Mf+82-tlt~f~RI{$TkXNN_E() zXqZNMm2BsAu+jk>0J(Zy-;@&Q*V1czc){OM`P$4j;E9F)GbkoNh+uKLSa3_Gb44@@ zVfmhB-O=k7CrtWIfEe>zkMAuCII|>p=k}qJRtCOmbc56ns5kB)kD5L`J;PU)f7|9f zE5iXF9V>?C`x*^;o~di5b^m6{<CDBN1o8erd->J)`3=lwxTYT5e!Z1y=Y`zwKFmCN zvX1-Y0?TFBV~^b+i|1i+j|=DBlkv9i`%n);I@UC_gJCmkkdxE6GlQlJ<;A0IySO=z zx!0FywqI+=7`nE5X(v$4V`>a(e=*k00v7F!KT0XxPepd0(mT|T{@Q2$@+q&G$E5f( zQc`^P;j@~KL-(!l==%ZdHGI}?KpJKH{s*4ZK?Q%|l&6<(Uh#LK#Bp%K^m#Qv6yWjE zVJJ>2&$<1Vi_R4d1HqD*W9My$q}<P%Ze+KEK+V>Vj@+HmmXP1JI?qz%e<rTa7BG+2 zO7Mm2NiMo@CKG>Wxk88hFAphZNerkJ(nWeBNtQ5YW-q^gXW6~78qY<tS}%))MH0Oc ztYVwbWl{yfD`-)zA{ZE6KV{o<DkUQ1Z$L@J5oO|OdAQwolx|#Q-R+%<ei^qmwTnKW zLH-P5ggOItsnVlIUg-ege+piV1FWLH@4X41GYmJC!xG_yuIQ=(ydqRr>;TLU8@8%j z%k(^qx!v5{d?SpKz07keEC2QKKg<lDgh(q!2wkhcltT4@d%rG^v}9}=d;iB5zD-t$ zZo`nWpiCmOr%#!`!DorEe^+YxQ&;$D*ZvHCs8jwJ`TzVHI&vk*e{zEBs6|~#0>Ez; z>aZhI>%O@QKqajF5BtIZ!TGDm{mli$O|o`=N~X-Rv+{G=pMTj^-ne;sF8-{<^XFeX zzq~{z{2luFlXCnVI``tU@xohx&W2smo<qlqT@#+2`_qB{cP<Ah$%z$z(mK~Q%u`4@ zJN)j0!1DW@S)aD<f6riw?yuYGU7bJwg9m?a9O~yTKI+u+m0|zMO}T6Umch101tG@U zp#S^~AM*FJGCoTOz5lcZ1_*Vb&-gp6pmyI`{_`_X=`R^TsC3+((SKe{b;Q|g^|GL^ zfrkJ28HK-|E2Qe;|Hrrysi629H*CaaxMoM4e#QkF1<tndf5^Inh(jMGt3Vo5k%@ZR z%tK~p1$eC}1-ds@Okyr(RFMgqmHr<VO~esp&M>1D%+@E=Cc@{7W0{&Yl();=$_rr? zx$kBSUAhnZhL~FO+kvuYTOXarly}gEYg&{D6p~z*`3W~NtUlVgdoWS0gL*}a0d~3s ze+~QHO#a6pe?lTN_IgaqufKmA%=ms1vv0hkk@yw<N$%(%$*o4sSk$pwE_)h>W|5b4 zy|;I5@@I{D^aTeSKwi|9k4#KV3~SK`O-K-+A9$)kUDgaq{8{lWBtjfgw*pLL5$Aoy zG-vOog+4R)(`=vko~I`&E!bGS5yY0XX{f%K=j8qRe`5w(&wW&7&p2H?#4_D#scfX) zs&p9^U!^c@+ApRn$B>!Q2tjcPCP3O9WM$!zO5y(M@f)fbH6tw$sh}0g!tA2^kw<Ud z{m3HN)hg)6$6a2f)W`RQpI3qnsxd|T72G^l&DD7ntqSRN>Hj<ag#X5$u&QK4!=(=) zr6gRne_h8L3yb9I&JNAhI~}9B*v|}eaPrpO;-+#m)3w523jWa0$Q&sjETH1pB}G(` zlIcfrwxz`3anhqd=dj2RT>!;JO|cZoD`N?Q-6kbRabH<o@q}i`PTlQ&M_xO2fl%BD zq`EvQ29|%;RYy{w9p#wsCcO66I{?-21r~QZf3p|tptEt6%q?A~x>oFM!d*PTjDOW6 z%?L;@uV%muj?;+wc4M>Gao^}EoRG`qV(ZOGP{G1=6!KA8T8hXmsZFMIAQ8w(z_W#u zuH5s<87&dQ>a;^s-p?zI*;x0%PED`KW&g$?OUHDZc->qgWQ?-m8{aD(d_lnX***HI zf2xbqmS>u2KP|{SIUSXMWmcA!IUVU}#-A`YW00HMn(*IUE(E$UZGM~hvH<k-gjr>* zTf69Do;wXK<F~X{dq_S?bBjMh6I5T`x>Grgw!8Pe-%}a^Ns``P$bGOl*D2HYoZGvZ z>>RjAW$sl=FRgdf<D{3?g|pg|3Hw8Ke>fv5*lH!NCY1Co7NiK0jNwky&MlcddwlX3 z^Cla>M*Q`)p7*nt)UAR(Y#1N~k`Z`%aCa-jOLd>%eH$Q^;%ghTmHO0#W6`f%dZ>x; z+*Q)Op$KYOf`U1voT#>PhYUF@Dg5ZPH8|#>2CR*?-+QI{{<lf~Pb(tH9Qpz0f5C%O z%NinqZQ2|~hlh3sL2gRV3(frPncgprJZNam1h+f4^<EXPO#b|>N2RkjB*Wv7r&sMb zW4q16#U8GjppKC<=Ypw1F^ZJ>NJ5@&N-SLt|JTu}E%7@@n@W)cyEh;@2M0@3&?=#g zVAA34SniIe7FV#=D>$c0^%$DLe{8sN>wWS`&HO{+A_kcl@<v$n22EJFRYYxV8wGdy z_AU9v;OAP7j88r1Lf>R-^rC5jKMU2)F^^-fuP|{M=Mh8MsmLh~RfFI8E}yYI`~lM( z46|R3caR!C7Nb9;>5xZd?QvoPNB5A#PF}qZaqCo;*zp`_|BM)G-bijtf0w)N!V`JV zZcm+A=j~}~;M!`m0G@DV74q5iY1SI&m%O21Z$p)PXlEO3s$2N}`)!$%PY4n5EkOV3 zJq2Z!#)z1Vz|5ZMe%1^oH8-ZX)0)=jfEEAna>^Y!2f3ST%cxq71)p7&ho5>l(N(61 zspGuDbY5@TQDI-}oA@_|e>Wn*%OdQaUFTZLKyfbF)uL}1)z56G!978S70Oc96zbNb za<k#r_eVe8US0<bz(2YJcdz-4#Da3B41U~JwH*r;K>9{LQFA^USIU?l>Pwz@Yro6Q z>j5>LH*4iA=0y3GOoc~5^IO1Sw@}jhQEo93$@}n2CaI_Fy@&tSe{V99i(%<e6?lIR zAC16b3{%Qw6Nv}kCVtE}y-#LWH*vW-^M*F_aca}|2>?NFuKAuO%AYUVMa@2We<-Z% zT2K}faN1Yo7PRCxB3RBG!wV$jYfImNF4u1EIGvh)4wt32<qsjc;JjoEz;q|6D75KK z9a(8iM}<2|w><3ee?xP*UvbqA6WJgrK}1rxE4FXr1fxaZU)pWI=Ax|Uy(F(w8war# znR#6_26%O~TfWAuJE0;nQjmB_dddwOqOOREzaS_~@miHX`y3p5@UBV4xSV5NhkENc zZlHBVhyrcJ#AMZL;rhyIOWK`LsmQw}hkcH#avO>R-v9N>f40GG=Q{oTtBSrjO3k*H zM$Uac^yRMCIR3j?Pqb4*rBco@N5eR~8x=%fIqDA^!3OJ{&aXLUd-6_DXIr*`mE=zE zoU$j=xcU9^lCg0^oH}Rf+oEFwJD*EfN9~6POpqhKJ0)I^PWE`?t*z<2eGHpA`<Bm) z#a8j&OL~0pe}#|OK;MkzmCcO-J^&nkuw4SX$!spb$?e-};`_7Fvh1E)s`43m+yM96 zI`iYj8ZY~BKPx=`ikS}G53A_19V00!-}v;d?X;M(WA-GMO-p%OaEqAr)%S;%>^0`U zNCW|4gkFj9O`&b*Z(ZT+#r^%9hVNy4jo6nNr@QZYf7=gJ>QK_&j#mgC6cuEO8_A%U zNxF3^`MzR{$`G@hM%cZ==5CrxS$Gqe#c~iBwSnGRW43IJpb_;2c)9)Eq@QX(ms2%1 zPv_I3#Q|+Dkw@IAb?YxLZa{vafmbvVx{E?jr19H#_YWb?vH*&V=^7bXA5GY8=uq4_ z>CfJYe-r1N3v#+Ry1dMu0DkaD+Igzq)D+&1P1_e>JtP_uuI?1+Cfkdz)OxGVz|i*G z^#UU?CP_h?I(iX|BC{i5Z?p9&fA~O*O(CzSV>_Pa9Un70+n(X#c~kOiza-@WrdInD zVfZg+OHI7TXE1NYaRH))nSjqz;%$pg=||kpf29X|nt9rprZs5uB61d~xqhB}Bt~he zW@qJ|Kqc#@CXd>Bck4A_KW~*^p_U$60;)0Kx~8<vbm)RhO?c!N9f=0>9b94O%k8kj zy~}lz{Nz&EjlCXzwFjubDQpo$*o}fiC^${u`x|!m+B+7I?(W*#PnS)cKy7~rOtjTW zf2G#pF7??PtdKgHV|Zs^S4;Qe<BiCql9N<=^Kmq2d=#diFgHziJPy17CTG^-X510= z^^xL})a>?O!}GpJj<j68279&K5Lg$SDZp3ci}VOJyvO6ZiuNX77ty4k<pAS_&DR&s zYkTl<JeRXQnMz7G^@yG?@kU1ux>K%@e{aVZtn*LaHKK>?8Yzs=9F#Y)PMlmwP0;06 zdcJT>5`5u10d5+<lG7;}Qv5ls8K*_Ft=)EwxE#8U+MQ)^TAJ55)-0^uI83C~FXM?! z+Y*z&^{U;ZrZwK<bGk{j^y(FilZkXtA6A^Q#RHM}b_PQ~hc|s`Fs2@K99JF9f7nhd zUKCo<8}cbFj#wJZjK;ucWww>^(H*dUG>-DBZ7Jw>(@f)xu1^z;x|%Z09&uVaYWUTt zE?bpYDJZIlAVqCnxmYF;a9XsZB_=fgu`NtqHbGK14qG_SSVL-bT$8Vzd~3hIpQEqy zwZ$jp*wTtzR+o<UThNY=IG?7Te@7*^MjB)9ccy!ZYZh;D4?X~sb^s}(kL0xW=4x&- z&IEwz*O_3dYLs(N|0!i+<sH^CC4iFJ9J8mP>c_kZ#l)_muDG?@YxktTpw<qGY_~Ck z_YR>YW5=qxiBk@LURn9QQ-4#SE!Ergn#!(mYVv2BFUVf9O)G5)E2$(?f1@7A`49%j z(#nKqW<no*=n1|>k&jLHjhYV}?}0m7)G-#7I)rR*%OH|`)GU6m4@$ar?^-6O-*drr zB<l)(?ky-?&og<@@YOZG@WYWX9Pi_#DROg{9ar8sRQP0N9kd<-R<`72_&87&<0>w$ zn)3F`m84G29jX;&Y;5?qf6OtIUDSkMVHuR0O-IW41AuSe%4a0%8lWb(-{aqB<>m&| zxO+|c#3v>WywItMTGdax8?6KeCL45<Gu7fb<|{iT(fF=b3hfXq5*Pk{6nFdCJL}Ct zwiWzH<p^v9(E&Q{lG7$5*1VY}G;~xWF+Cq%5};yvAFIbE<bFcuf9<0x>KUfMAEvIl z=g)20RggQD{^50vxPT~}s`_i1DjWSyU4bY?llpNiRh*Z1wv`EQx5<e;lE|Rg)&CsB zsVvWWysKGOkWjc9PrNSO?^_vka{bgu9O8+&|H+haOjtSC?slxOyA>gWkaSG@?Vz48 zN*;GccZQSLNxpo;f5!8hJjkeb0>kvPa&2MRkY&nyoHXrT1|;A>?X)1po5fsC1S4uA zK#y$S2)#JIJEGkV<e+6bxRTfnk@W@9`#91mVkWDo@F})*@FfDDdsb%}K5tU(e3Skp zgZOo-V|GUGwM$n5Z$UTB6~lbg<f(LOrfr(N%3X3~-HV&cf7daQbR~sc_dF}M6gR;W zcepICpVtTl`L*)hob~e}zT|i|a7kI8WH<KGXgBTf48Jakc}oT-RuWT$Z8&XhK~cxi z@UZYa8#?RC$}EurW(>@<L`sW4E0qLqwh{{Fuul}3Q;t5+RcntXtX2Bb8w$^j>Wy*@ zoZKqID50$7f0A#+r|wBq=NWqGZIy-b=D{4VcG$>!zbw6U!n@(SGZXSl_t(?J=O*MW zSet8ke&Hhk%KDVRx+8D=rDM_=^nNRgsmR!jcqY9IBr-}{?RplSDMib<8lvuKNvay9 zjASf8qh#ik;)$Bjq)~CJAv)^3ZMBE}xsEsZ&Y$ERf1dp_$}K9&>5T}k`>$W}5n73H zLOlDf?;IE`n-6&iOoU~_!gkefiR>62Up6m)v$#Q-{r*sv@a-?hQH=CA`$T=&$>i?^ zUOs*`uycd`A*(%_cXtmgynkiQeJ<Ym#v{&%B{?m&YKtd;-PHW#AciahxxP&&?dJR0 z;8zrCe>M8&XUf0OGeWzz$$kz*ZLKeEv68Nyz#EzeY(_{|d1eW?-$*x6$>}$dI5O(I z;l0U_2{DbKMyoJh7w2ZOO`GPWAO;$WZT%kgG#e{Rejz}t6iK&3WpTR5!=XOHa#e4E zu@8t$4KIxw2K*2lwyN8#qS>xyOuPF|nBZ(Oe`T3e5Bc>z9W0FIm{nwflJ+Rh^Wt)j z4p)JA)%pB_AG7hgi_w<brJln1uuJ7iC`c}ay7#V2k0j=;e6tn;lu&R4AAL~PvG`Fh zWRulfu%4!;gOo;ZOPfzTVZ)X%@V)daa*CcU%v}4ax!UvVS1B{EnH$(D`3iSJ&JGs} ze`(-2F$2we5FKYGVBH1_S{j<Q2lCn4nz9I{Np9Aj%<#P=Z4nf_(6pg);(T9#?U>!o zWxH8aV7v=hD}XG(Spg970t7j(pgu!3_x`ir8tzhHp`50=ebBF<h@Y9LPKRS4xk`-Z z%2$}!61>bozedHVnNcFJ)4;M5U!yiEf2B$4=u|)7(z(V~v>Ll`X2moG7%R*PTrB$h zdT>|1m2N<nq{JmDn=+$)-y7}~`a4-$THk&Mj{9r5cs=eLjLpuZ`Op<*vWL?45X&j$ z0b_#yJ{&m)=eXhk{WU)^Z)BkZab1L;mMaFtOpv;Frf@tbIbq?=Bw)WgTWRxsf0ABh z=*3<9&=%h%-A71c<^m!-y{rwZahK&MTZ579oppI88=|?FaF?tY*ZZ<Pe5p)+6529q z%JOnrmwIn;&-8G=cVvz@f*(?Ymia#oY6^ry&q(vn($UbSeTzzuN&nI;sn0s#Zb(7B zJ|Qltn)MR&jnC`cOkUpkCluZLe{SJ-S?ABZr!}JL7}{us<XELW4r4xd<#OGH0=>bV zkBGF2g}NBW_*6<6h-juVHtiDc2EwZN&%Q|>xH}e&(_@xMDcp#Vo}IA$U<jx3!0Qb& zvORS*<r{c+fA?E##V0-O3ognekn9qT%YsQ{k0#eQax9ha52UHb7#^T$fA#%{{kCuw zQEQ&S-q7N)&nlgTOAPG6CIcXr8Du=pO=~pf?KQYiS;`L~5{w#7G&`5{^~k7sxC9$W zTfOA^C?|;_ICnTHIJ-$)hu<)EW3)D7kYtWOS(c@~S2oTp($1!GWb#~Lk|ZreAKLex zOFyzjQsY%roBZweVnCBre}RUg!YnoQtzR?T5<sN_1Tvpml1L=lpnteH=W{(3@IZ1x z!d4;b^vEG-C4}u&fzw7}$4Ub}<R8gM-Wi{qs;lyI)fM(}Odtzp&@BrhiJ6f^Hy?R( zk<sra#03&z4sib5_fSsZU0JT5E~-MhucmF&J~vijQ;}ZQiZk!Ge-`9h%o#YGvaE{K zWx1y~dG7G0v>|kZh3iN{nb0@)%k(J@NERKlJuqIeV+H2n4tzsy9zQ~Cv90<ToRXXp z>!;oy!q&eMtee@~d=T;WImb&}<kaX|Ef}0VuA5nwpQNGGQgpgVI4fPmYgWlb1$Tw< zlIv$oZGt8T2n%DZe{5et&oZ2=O=r;KG;Gi?|GMMbNZq7Ko|+2?3(lP4to~Ca)|GM1 z3|SARI37xBj=QNkJjaJ_F{gNAbCvB*ed>e-*$_{QuD7c8V$|0<8w^-@u|xN{7x3n> zc4PY^>ZKXuInAa6LF0@Teuu1JQRMYR-GzxyA@;7k%lCD*f6wVflhve_WpCdn5bxq^ zbBcD+=Wd(rM?StXGvy4pNVH2Atd7t}mHQ#l%moB_RPt*~cJ-@ICayhRyY%!SW6E66 zG0}C~!nTXV8;o}PG%dkP(Hh|nX_SMG=`=t0evHPBJKdaQ(B^sM<cJHLHr?@NGr{;T zL;kY!(qAY$e=2`8zH94d_TzL3;ffwE&6C<`^v2oq%W$l@{-puQ?yaINK^#}b#u8K1 z`vo8U#q_RlpYNf*c^&K^{1`0yx>uw3cYU#B4?S2eTLO7ZwKI8#pVbzpqJqPY$RolF zR|qN1K#eqoUOvvAA1s4$Fq3DI<Z8KUM26cdgdFbqe~R5GDBF2MB;eL7N;nX|cTcDs zc*+p;t37D08ZDY<-hE+9Ks;5+djP$4nU_t9&?5Bpimf){bu7Ag!RPMfC>0O!zG&dH zusI?zfUS7{j<4bJRSa8=uCB+9NCEl1bN#NAN&#ojmja>~va@%9-rjz+%W%zb{BDZ1 z#H<V?e{0Vzhwd)B-{-sn1RgQ%GC>3U?*P06PCWzhpTkLc@KlZCIr)EKzDBES$W2u= ziPddQ*`SsbmrFw|zcZs;TR*kH+9ri#JD;lVf++yDS>b+`&%cqr3%ZNA(911=k<5Lg zRV}{V_>%E(=v2$WH3l`#`eI(?<n>&FS*A1oe}wk*rFTF^I@MWEGoL9ft-8!avus*i z+{VK-|6}AQo6!O95`1qaw(XV%%cSlHOY5)#F2NA8j^G<NqD;-&)Diydu|&y=$;|>> zPHP$wEMDJ7D`AD^Z9JtIV)=1z8D@rjvgSsRS}h~CR>um)_|%!d;7RkHb$F$RBl&~V zf17KAdbXlPxtKB|1=b1vKvtVt=N8jNk8oxm;xI01g8{RXE|RmpPqtxw4pXmrtkIb> zt^@QJ3g(m(uXyQ^9Bbl#HjFWAtQ1cq1WQ(SFXk4}YMZ1pPKRy5rdIQ1OL+&!E6r1@ zuJdL&8oo@s>E@(|o$tkl2_-Ykh~LS?e>;w!39C8QSExYkC9{YRXwO{4Z8$u)^gi3{ z1y8BhM9&GKeC=X%I}(=Z_!=xP_M@=m&E2H-Wlz_qCbuj(zFrBNFh?4tC_hTdquQMZ z4g?GDF3iI44~f5c%aNCF=PsTfUM}FUWGEp^-UMCgpNni;4Da#QY0&9=7YrY-f5?UU ztC0SzJ0%FOzesn?t;`&ZJ@l=N%-ow3Srf`3w?TSq1N?}Dw<n@-aPLZ-@~K{fK_1(P zGR<N+{dTNpO36H3VV#4w2QhC>H64?VdOYean)$uPyHg<54@4`$=Pz=pJ+wd9>0677 zfCl_QpC@2%;{c!DL=Eh0b?%}Ze_OAg`}zir4$IG;Y=#7>T19`+qA5zV)kawnliDD5 zlAeuh*uh7((MnfQ^4FXN-1=yayT-vknUgSiCh$wfQ`Fb1Z<><rctYo$0NL>~dr=^v z8LrahnSuIE>jWcejvguR*2;shhh_(=ZOk@@chWH$%wx=<=!HPsxSJxFe_LR9emg*3 zINrWgmq5|IEO)e2q1c6fKT1}}{ial9szp61kM17O;IwoA8uqJI(6H|Pbk({<?A4S5 zdF^qUOeyHxEv;bad*a@w^^=QxXQUgJP60_1NV3y!gj$r<Pz*}MAE(#o?Un4LJrH}c zCLTI|Gow!$7O9!$i!biZfAUdxlwv6)@p-m@(e?@C(?p+`o8)n)HCi_%=2;2QY|;zn zaGrKdN8v`#25n)@`r1R=<9~^vx*92rF5tcA*qexrn*3MQB7xctHbu)}If3}HDwE-U zp#hvFX&A%AMSuThQt)z-^8aDyt%BofmIYmdY%w!4GqYq_Xfax{f0((&Ot!_$%*@Q{ z7Be%pnAKut82@{pJLks4ct2fwxNqGN8LKL@vZ^xIw=(ZWMn>#M7%>p5z&w{SpSHnE z73r+Y*I3KBm<#d)8q<7VzRY}SXam{-{lhiE0?!&Y<80~(2{U&x%%V3StXsSIX2SLZ zb-B-Y1p-%UZB?1hf94!JiB7a8E2Yha?UpwS)(_uqMV=2n1mJ#`gQlBR2XJVzneRJ+ zSYuL3E3r?V)f>H|9A^r0OGlKJTy#=fh&m1qRAHAK0?BI5xy3D86jbBCqppWWLP#s6 zaglYVAbz@9nUI7z=HdMx&9StP0M*&M(8v}JuO$R+%Q8Q`fBJ_Jbuz@FUF1kNJecx* zHFYS!c&bNvNnPfyx26**Yoz|+WXwIu2C}iq)HD_bBNxxLOOacuGcWQL(Z67dCA$9p z!W_|in=DvuXsGZeSJCO~$Li1<6*r>~4ZnWbPV)5jC6Eo?P21*KiEwN917$lGF(zcA zB){c#j@aGKf0}$*X##%D9vc+vP2i3{lAlWunrrZJ3y}TlG2C_?mO;JCOeKkA5mG%# zB3I<4bHsla^yj7livH5`Eax{G68syR`jSDSm{BEoStU3Ro3J7OcQ9v;s=uFvg(+|J zcgThQPbxwk7{lN77h?FHDqH+lhMuM>PsHEJoji*Ef4+JKj>IX{Us>5NtoJTH7D}5O ze`k|?B7X*QHpFQ`|2y2%gtD2GZk?ZrB;a+ZaQ(e&_BW*KER$`&-9I;f#|L?~Tm?Ee zyx$7_n}(fGl?=aCisaR_{GP!^4LLjiLDCZcj~;W4s0QwTmnn^S$KO$kNU7fYSEDK} z!&G0De>FzHjP(3n2Jp|~0BBms{%$j{#2=eA0K_zi{w`zr58bvD9m4z%n3wAx*a;x- zzWfiE_aDvG@TDt*|LzEl|6pDKehgmRK0<9Byg}vO<JcaYWxf$GXe4s0v*uD-I!{j_ z;N)nQX;_t{p)}rHoxeyISbA=fpthhg=e_z0e~Adv^-#mF6!Q_Z@qi>0D-Y$)1cfb> zargiEml({>_CH6%CMT&}Zxk4)t+RhLd~lA`3Pn=ys+=v!Po&aYNh~Swt<(4~Zu7@P zoV+B7XDIj>{u>+myLyVq46+l>+B;UY7EwUPhhY57?*IVyzI9*c28n-Q;Bfociv7w` zfBpl_cSP7PEQIigxS>5<WrYVCG2~IZ_gDf=d!7TXV}w_*`D1vpH!6ZTU4JqWLxKh^ zDHHc#WXr00Vh#-GP=-i4hQ2JRrTHBY-vYycP?uubC?T;swn-#fCjagyEx<{jGN{bH zJ5hW#=kzXAka`s^Ehg<Y)|hN5+L}K<e@=mUwSvd{CVoBqL=-&8M+g+-+g%ghKw#VI zrifGIYZm}5D!GgFtC|P|Z*O1*5qM-Lozl3$@@HPEP;w-e<$ff1G)$`aDyZBf$Cp4r z7#y@fq*SEb1_pj__WbRL0FLS+O7Izk8+=F=*wG!pfnMeQ7!>gJpa9fJ*MS+ve`W4< zb<9^16BPH~3r)6RgT8#6+;#zAH4myT7~iNvjk7yOt120Ax$U0>ys3vNW{D^%i6@#; ziZj09<PcKx9V=ESTRv$XHIqkJ7cSn;nKfzrxw)Tgv<SEg@-pyNTJQk#`mW8E|7=2* z-$!c?39+%h4OR>DIJao`hT?lAe+T`-|3@zXk)6qzavcly3-1*p7)`gq0>KNhpx3*_ zQ4<pyM`4mHOXI?6llXOe|I35~-u5Xa^yd#R2h0!fvk$5j(!8qcgdk0T@|;5Xa!7hQ z0l}!-C#|hLFoB9$HD5mSD`5-nk`n{9D<NO7#Lz*Ar&rijNahs&>uZEDe>#%`Kc1RP zIe=&&(z=wI8vp7{w*Lwe8h28+<XvO6!e9FmZc@SGaE=8Tr4!^KCT-LGJvDHssq)}+ z9hw^Kb0idJ(!O+w)2icG+@8^S{QkntrB8S1G|iQHARBXi(w==<LLltBj8LpIfvhLd zkiw~Iy16OqcWF%1BjIWnf9wd}8)1O1imcD6BQKK&Bv<!I!(zm?+po-l#(`PuA*ltm ztGatY=9jZgA4sn|-}^00woCBExs~6cb<2(6_1#olR4fr(B>yC*JJ0-1`lsvZ47Km> z3R*tdEC7yJ-|yctc#<#Zbn(9-$rzv?9>$bHrkN)N>xJ3mZt`FGf1(Z!@Nix2x7Vm7 z>kgPBy26Uiy+YIS=8VM@M?V;YoveN|IS0#84y5V=>y%ntSz6%9`h7kTKkhk!fF@g2 z4Z**BD6$5gkA&7k9Mkq>$HoM!OsV9J|LsvCU%<V_#7ieBBJXt%cwQXKA?_e3V}x_p zPii5&^ERTXt-#z8fAx=Q?gm;V$A&kkKdR_Nj+Ik&eaz%{H4WMC(ubxa9RVZC&6q~& z&)LJ<Ek~BF{Nx+j4j!vv*BOqJqnQUxMIi5rTG|uw<E-75k#qN_O&E-R^-pO(OlE+l zj%|s1m2U3=S!Z+r-UTn4b*a5D0QN-AtiJ7x!TS-bdNT|4e@2SjNT$j6L5M!AxDPyk z_QHbDKaQ%VsTt~iwYQn!x>k}iWetbEsyIV@-*^y6>8U5#qxP-QJZ5H*1IQ^Zd6O6; zr%=@(xwAOk(}OBMrQ*j$hm^G<muGw}4^`0Md9AZci=uNa#-Va|DgsmVoP)7jYx4rn zrsvi?*tCSTe`)N$8I%&7?Uh4DENH)M8Wm195FM-t9}*l5u>zoqemcstUq6_tt$cI8 z{+J|1+c6n5MZKuN#{SqK_G^<rn1^0w6RFiQWWdS~EF=2$x21%Hm+;PO?FYe<)o;ti z!c?|iO>XaiGp@AU;*-a;_V^!E*u@Z!BTO&h%)WlHf72qML#-eXAC8RFU@OmK*ucK! z0l!`*dTA~eKXg<xT49?;9s15-`Q>$YuR0pwf^hwOy*UUYBE##AXa#-|CR*UEedF1e zg(1Af7~Uh!e@)xKyr-5P#&8X4NjI)ZrI`yKBh^8v-O)-Brl5l(^s*SWxPm|U<*}Zq zf`nc_fAhOr&H(1<xp1e9x+COz;B_MQiK`vxh(vU*4vt1tf^VT0>TTZrh*6aJ(=7aA zsE_)SnUb3wXKVkM1U2NuDsT{&F~%udkz1$aZ2Hx5#I}FUpO_V(0i*OT+y`0z$JzFx ziW`=P$Ii4z-WLYBvF^vEI0IUHU~0W{g!J^oe|)!Pz*6oGBXd*d;qD#+<?7)SImQ~- z>ZOebQjz~je$pu!%A~+0z<95+H<=5Y$?z)>bEir@{fqmlCtEi&85vX;hcHnzpV+?j zbsG+w$FFb+f^-Kc&8rNy{b>PNM^i;miL?{RO{d@)<{1X};6GPeGZidH0QQoki>HLP ze=j_@LZ?Kn+6uQ^+5%-9ni3O0s+N-6jIc{@my?iYY?Wl+`>7@(=;X#~d9^QP1mIF< zNNg*(SI;RHm+0$CGYx`XD$hY#h@H&-jiFQtledH==pS~*O}uXF{x#9D3R%;|^<e== z;<i<=%54yf2f}VCfNXDsiNB&U?tuKHe+f{bLj%91Ui?s|Gt4{+&LH<Y&kt`@iNq{N z7C#Cw?ulJd+0nw>dL_qEsK7+_NqB``C6>9vpS_DV81_>e2C+2oQU!2#wnpC~wZP99 zqRif!EccwUYY@yL4PA)fP+0#|Qr~=HV_Ubm3iCl)J8y6O$xG*$T;nKZRiE-ufACDD z;ap_ZB(P_G<>Ef-9#Qy|(M24+ddC?!y%<ge{;s>sJ?$OY))r0U!R~u)eDYo#yVqGe z+`_K7xj%@^?ovMKiJ-v#b>vUbtV3=ms&h11aBJ4(`-uey0Y}K|zTmr3i-WEX%iJ9< z0aIhPeV6KJGlX$xcTK@0YTBRBf3HUgG-Z9p?grUcuVF4?=4I=RBLSwW#E%;>KajpG z6L>t5>-xd>hklH4gV+G06(Qe_kTyvj0)xW_gSoT1m_pqwq)%&TMS7z3xZY*p0r%*I z@3DZdR;*v!(*kX?a0BtNy(6_fE~%K%2t92jRM#{vNuPhe`QQg%9~W>zf5*FE!_RD8 z06_aFB;iPPpFVH%2g79gDL-1awFu7x6u$J9GireD_{+gp$xgpC;9Pq=+L6Eeqy#6b zo=%o~vOLXfA8;KYok$$`atoHATq$d*NcwyU*{mOiZ1@FpHI1B6ttXm)<HSgwXr47< zstT&TnL%yk(JSPJ+^{u{e;!GOb8xZqHX!T><cQ|VNOXLG;(k#wJx*wa4IzC&;h2CE zEqM=L=U|+9nzq)<H?zJ<mj{2p!)u;)9itVWV&&0OX%*ADCqWq%`8*y<3x=d1v-r$d zFIBQ<gtu)y($v9}^u;NlgAr_z4J?yK-jIt%CU|QfKV6HSQ^)({f4Kn3w<(X!uc&`~ zehF*|QZRIb$J6sL;pBXgsK<vKMmtQAJ1-X&!kCNn=#o4Zf}NZy{9M^(f4VYs=YD00 zB6}wzk0|lzx)biaexNpb<CvuEwD>)LUwkW1G+)bbMn*kyxTBr%!@*U!G_)GDdlMEa ziRf#MItw*<(w?Luf7BNF!A(Cmjav=j&^>x&lf2mNbmgTs-O3Kv`cyi<aUtw7((WK{ zDGT{$WWgKM_LXz}g8hC?C|~h^rES{Y9|8pkMn?m>7T_+thKt;*<A=x_zfbHXAX6sT zU=&95_&$bRm>`V}8J{sB9R{pkqVQ%CdE&Cq+-_}R=}uQ7e?s^_<*c{<4y=drFIsIw z7Y^4A4*E{iNuVSwPMgvvYIS2;+wGAPS2mVPYdwDj1dM^78QRxT1MjHaz{LE{$EOKp zM3M_ZdpS#~--Cawp%N1Tv-0mXwJXiZ_Y;EBuk~kF+nJP?;&YR|>EE~ucLjd|a)iwJ zoYANd?-5MAe~BoUub}2BUnBFwL4!{9hMM!F6d}}p*FY0R8o_(Qc_PG+pd1cj1v7%L z+{=ComwqGXYM^~cS1Gd%!5j=bQaZ(26R8yd3xV5s#(`qyJla(#QSTsVtjFtOKCI=- z8^0aHks$39*YJA9gkH6)IB%fIN>CQUjKvsNcEL}gfA4##`p+Kt@~!v6PF2^WK9<Ck z08iAG!uoG&i<MLOcY7q0T1fbiF4yYl$km(cA`P&deRS9JJMo7%8KGl_iB#XU9|NrA zD*VGw(67gQn0jrH*(}}|PnM!(Mi;~9p+5%#Aw}mn^IZ1ndPm1zhF<9{9NU%Rn;YEL zmW$#cf9wv9v<Zjo(=36QLhJe-3u=rw#{l;6@0>MOhRCouPUD$?oM5W+q*Ed2qb$Fv zAs5!m8vxuHHSM+5o@J<F*4qY)_@WbB_5rq<P*pTEiiYs#f<)>APG0gzTV4O=jGb_v zCtDo6XJ4-?4Gt#mjV|mwdM7VfYR#Q`{0p1!e|qBMTW5n`U<QW0Va>l`lW&HH$>9bw zY^rd;NM>#+qV@NUDaBkCDhD6VOk)d&NYRa*aD11%?yHJZIp~)D42vtPri0SJ^M*3+ ztP}c?`HrlzXh;f3yd3B}5Z6TpEW6Ya>BozYp0PPCe;R~I38%EW%U$-bJcm7?()+El zf8b;{*Ou(@ig}zD*5=`;!2JlF8>E%r!Pnu@A>c}$`1Zh2`(ZZc<&dDkQP{i}?|u^J z^F+1yHtwNFGx2d6h-hhs{*hs7ra|dv&#AMceWm&b)0=z58Wk+^-I<tVbI*!yrJ@ks zrr?7p{_d|zLRNf<`NUn%uRHH%WLz<Je=@8B-$A6P1ZS(M1~whh9<5#8hf_N$N{lm) zK(sENS^6$0AB|kn&U!tZBJu`!_KNom6gI2Sg>$HTkUe(VOx>!p8`9s2pBENwyb~m- zdvxh!ao7ku61J7iu0%m$Dww@Fik6vL+!fzF;O57z;NUB=x{96ox-?3YU4Upve+AyR z3$@Gwqv8K7%HudY<Gmw58@iz6GP=qPW}Qr}q5R@42U_-}{vIW{HCV~BX8U=IT8wP+ zrFW+8M;N^GvO{YjO@ZPYZ;q9#D_n~ITwE@T!;Zx0ijQK;E06D_>_Gu_(Qx~G9W6Y& zt{?B?Cy3ZrMdE8#iOEa!sIA{`f2UoIj&UqM)+E!rF3LHqS2Eyb9+b4rpNh$N+VI>e z7Z8HBW%#yeLK!?R5!_qldSM9M+u!X8y>*4E)JSE<hM6o4*xQ!<$wAie0bq`Py(oqO zSVwH*(f6^s_+j6wcDE9s@nrTjfE~y_8Iv#<?$j5Gkf-~JUk`3I`dn-le{J(FKZtXV z&ZlN0(OOJx>v;B64i&=xcqZw=Cs?2`iN-#0{}<4<3<1f~U13Fj_OCb|&tp-JuyE88 zC;VxYk4CDGqPF4!Zs!#I9TbGGJy3^C?_GSB`Wqeh$5E#AINPTl#X~4X77xsNX-8<C z3PSjqj=3Nhc-%-BNNew&f9n0SX_Br^0*$m+gnOTn6;0!km``@Pa2C4_NYX^cKiRJ| z4spMQ@66_o>4SK{NW4P5&YT|CJpLojwf(;>1DSQi;2`YTC58e#Y-KL~J!ICspUW59 z;0Xt(D7ly&+E+R0zVfyA-fa|{*&Thak<?PLH*v5L>gG3H&R?<ff1IX@%IF8Jp7jk6 zIuGU-DLAlmiDf6~Qa2<NBvTt>q>D)oxK0r25yM@auJ!CGT1!hPlQg=SswlS>PkhI< zrS8xQy^1Dp1z>n%3BMRQ9PbXR!+7pD<}8TQYBIZU^YI46m>^Y#b_X{}8tr38ErL}` z=l@(oy2s4o;esF(fA95ShmslH;ej&&A5QQm(jGg+fU=;;<a?+*$eFy^5BpaaFF%%5 zCr|IW2aD?tY#9g00Fe~8995_;JP=y9k#nT}x|j`c1W9!xbO7I?F1}^dR%*!yEu+jD zQA_L!Z}wNPemKB1B*EBZ{3)W?t2;`vpTvDVO;3)wU*ZhKf8^8ylgf<w#wJ(0vvOTX z-TqzgCx>Ion)W_FYsYZVNh+bkSXoK{KS0300D4L%B??Qb*T%l&g8j70+CPz%e$8jY z`DqGG$Ke*tW0zC@TPJ|7BvQ*ZDl3(80Cw+&d#@DL-X}Q_2vz)(pt#l5t;lyUKF%!p zd**ziUj2(|wFV1tIe%t-0o`FunVBp6$M*bR*4!)pM%3dU7`5}Fy}oknU|@4|KY{Z^ zJPtriq{Jb(w>0&pIJ}$8$u&nXsRRlV8)aGr-tn9-#5g9IVHMlkn`VnqMiYEhzVF|+ zk6WH788=D^kTV`#M)`6M4$_FnZC)G@O`%N)C4`1|w$*zLNq=2_S@UQ8-7>hC5Lv;E z+&_MrF?F|8A~kl8Z*xjWxxilMAJN4P?@Hd|i5(bG$Op%EK0D0uTn!@q3;<@3O$n)! zEf7ro%rs(vvPMD4_9)wi+Ug-Y_k2!EW)Xx5T)}6hnmCo?$kP^bssfAJQA4d6eURPI zq_A79dZ5&G#(#*cTF~`GM8uy4{pPF=!S}Hjq0gOM%~_?Mew0z!Q7M=gJRMkz_*DJ) zG_!T~x?`t2?+h0T#7;vyIm=Y3$4Iz?KMi?YxA}r92O+Bx<~0w$WQA7q*5o3=E78vj zb9<>6OzaSlLhi3!6Ishb?wL4<BHA>y&Q_l`Zc}Z-4u9ZY)}a9>VL5P{bof=rRFShX zVrF;+giEU?sb48RS6h}F>14FYn06f&;av1x&r<8D<%lp(j!cEkXb`U=!s=if4ERS% z<D;3%Ae=bD)YMC)E`}nvN3}Xe<pt=NOS<nz>bXI;3cAtuElf#NHB`usQU^-J@BQ?R z;D<}cwSSm6Sb%+rV5Sf2p1Dh(HDM7Z6zh+vO;lAXkvqSf+3`?Kx9eNtKQ93wHu%*? zb_RbUjDGAwH>YZrN}+|5r(Sh9e%E;7kkBPgvWzXW8I<hK2WK=(y0jB5qp&JIMN?m> zaokh?t^*L!l>6eAcg^KwwH7=-hZlZn3`d=G41dAqK#$x?spfZ@X#iJNa>NaV`_;Rw z`chpSh$`UTTubrbCHO#^%8RTd__z^nLX`~p8KuJ@YU(Xq%eclxMdS*0B!oN?3K<Cz zxs0{KodKX1{b`w^@I%%{%$+E(eCL_XHJ4Xpk2p7YuSvqGkcS%XE=7e?uq!CseP&NR z<A2D@WVA=2el6@p&BA#N9Qk_}^c`lBeoOE%+x2YiU^Op7TBQo};fFkxo&5&~=fE)I zvdO&!6FHbe!f?5&=`znPejU&PC6UVf^QA?A4L8F8*D{~p?kE20X|TLd{9GeQq+?h^ ztcs6IRO6bUJd$C+VAIWEnFBBn2S)4b_<yNVj(yZVm72%r;%Wdkayxx8J|1=>w2f*Z z)?0+1>9MMT43#uiHCRi_kS+m#3I${~Hi?Pp0LXMj*23T8v~0<1HXXT0^ZJpOAhKGT zV|;jSv*X$q-FY##8x=i#rB^a$mBvUZ9$kd%IrSmFsJk^%wblI4Tg0BpcS&Vh@PFzX zBx;+3V4Q~pf)6Vi?a`SC3!XQ{;W>~T&3q+&L`Fd4>|AWcKb=VR`@zqbZ5H<V9QG*c z%|^bn|LQH*+3Ecq*NZQ)ON+!Z0AR1M)7+>f9sN!H#GrmHNw{q|C9W}&$dXoG4!vzF zrRF(W?q{T-NjIwi6|a>2J@P*#Sby=I&cqL#Zs+=N%tWwxZWOAkX$_>BlTZ73-F^wF z74LCC=KS+F)=V8w;;o?%OA3=@=OM%Pp^A$x2Z;6g*Sw%d5&^eDS>*IaLVW0(0zITr z3F9afO6Hf0ZwiG(E}mOPxq9#GneS=YLbI#%0F;_5XyIcjz>U7=#~lQiFMrdK&X*FE z3%MDCMAB9HV=A+o6P~(-t{YE7%HxX+T9lVP;j51OlbNV05MQ=A7jtE4tU&0Mpl}mp z(r&8#2ba2P$4a-8GcMVXS}CmGGh9ZD6>5c4Ogk>?44<+{N;+YTx{Up8vlx*7`JVB1 znwirj<A&8kauTc-me9`d=zo-<rHZWG{^T#)Q(F`?L-s;%EC_%kk(o`Yw29V98h`=B z`q47mV<;Vhux(_ZEl%kUy9JtKkEXm)RvQS9m|~V*{q9c+3;vf>%8D*7ce2bdurE0y zqDFi?vgpf)YvlF99kO|=yYX3h;bh-_XfslwSJdl4+ikKsdQZblF@O0P_pCu`<HkJ$ z-WFn&t$lrf7!sd9rT(k8pp<Rz;$)TmbTeJp%3GY%zKR(5`g}ZQ<Ra6{p)kp&>g7^s z?h<}LM{4J)w)^UYN`Id^D)vHj#p&AIPE@TUy&Voa<9AYfC1XZ%=xe=3*@}OqSvkeP zyv1l5TG03rFel;8K!0p$2i2XaKC&A$Lgc@>6O;=1Ixl+5id5a<lA}8D(~AAQ=3B)0 zZt~#Mfp(!e9vDbrJ)rfHW)4>||KmP{mwU#3E%*kW+xF+A(l})md4lV|UzUG~d$-gp zQ3k$r+`CLx*nKWDxGK%9c_xdy=<d%nzH0r#Rl(fdO}iy25r4n_JO3mkm;&FX-P|{l z(ndkenpzI@q~<03<Rd!{RDv^ZbH`BL4(RE3R*^Za{{eyazXn^rDqP#5e7kIe>l6A; z1Qn*)aC};&*BX<ES`sx^A0Ev7ekgev{msh|{uYppEox$%gu8A&^i4_4@`<k`vq<ho zHQ*zVL1WL*sDHZ(-xMl^^(9h?ORI*}gE3M9>Lq!WpTg*<ngyx}KtlM9jH7eujeW6g zhrbm#P9#p{Ar|>$fZWA5g-vLj?5yIH1%|U_^u&1I*mVQ%p4`8M^O79jOr8DsB>XYo zEBO4K)=t6@vC`(xjhW>j8hOqt5Nm0~CmD~MY|jMt`+v?hx#<`=W-~Id>2jv+6I{Et zD05nA-bVQEWY%ifz@_7^>gsXx{c2h{4At|}Tw#g{ovCYj$g~o5-?s2S+)C-gXHD(l znIpRhr}@;{4ZN6ZGQ-dV5}@7U#}<MX&KYrvnBYT~(Y46qgjG}p$@<}uA}X@S@>)!_ zWCbnxwtq(Y@={+vwZx?W2<|8nT#vBJPro45sg?1)`X`b|&qgbJQL|0D6Ivt90<y@_ zQ`rLRgn8=H{(z7?u;seaPHF2Y@J>dLq|JU>%JpN#fHeQhWxs}=kB|c;jH+(dO2O15 z`5nV^#($y1U!8sXdhTuWH9F4w-i#p0LbWvs`+u`?T`dntu9tW%XyNR>lDr5=$gMJ< zRPVs=+tM%iZ5K{2KpT4j)nXqAk?JZn_nNxV5%^|xpi5L$HfdrL3#)AgC7;0;KqZ$t zNKB1kdvdbeG~xG43OjAivhQD9zprepX=Uq?Wg8gv9BW*8OWXvzL>g)OxT_akPoS48 z*MH^|B%lVKO04r(ZiY(HN#^Q}uFo*ff!xQ62@u9|ea?Jo7APVOaUMzoF`TXwVuG$$ zW9Ue}$MvYF74=tEYU8<!EAL)e9pTMw@cP(f-+(9OJJ`~l?CdU0&?bpyhE+>*Bok=+ z|BIJyu)%73y;hjpEC?2w+?f}siFmX%UVl%E&tVeI05m1#opo#qBW={c&`jgnpp`l4 z3jbETQ3ew%jD!$u3Vb(ksCpUw3e%q_amxRL^&*q;i`$@N&8NQp_zb!o9*q588K<HS z)U!vpI5F}5Ncx-_^Fq&`%>RU{bG!;HD8xYczr&p9?};BtS+gul!^#Z)%oj{*w}1Ku z-fm!5WuX0gC8*jzwX3DQJpZA{m{au6gcn8?YqI~2x78^B%&Zh``}yx83kZKQDjjyw zK3}%KquPXj0JL=Y75iUBwArB+ADqV!FyV*)4xrWkp~7^m;}d@su^U6t-(TSTV;q|Q zWSkYnxep6pU&$pSwc!nG!~d=w9)A=bw@rDBhs8JN>4!O>^N~zmSHXYa_I&^1_Gt+J zKezvXkK5<1RfQh|-LLyanRQm}I8To3XJo3J>iE2y-P$~i%9C_0LU_pk+hbTiY&`-- z92xlWi<lGj_L~b1>@bg|Go&poF8cr}nsjavq0X>(oTZ*oBU24{Cs^v^e}7yk0`1QP z<+d8Uf)=w$prE1Y-+Vpv4LmBh$@JpnJZAwzk}K!+mR2AhGy2b;p-DM7RBr)$k=x@` zW0EzNQtx~r8s>wF1dTs9#RozO+!+R%Hd;8>`P;2dlNCq+^ZKP(hp>nePm);M#j9WG zMnt_7Jqup4b9fYu?NtqEUw_4h8>!akT1<IwVFSU&r}u*(8prta6<X#+ODpbpuucSe zBSS8dbBFpdFXa1T_)2{e1^Q5+@iJ+Kmj}eA4^o!+8~}kQa1M+WYO6lMpYY}J3vXr_ z65IKasdGH263wcHi@e3-)<+sdbWp$#r`sKw_?%s%jY+sMi4C&GJb#P<0;WrL4!#Jq z-UjnwPEIv+ugdFooebzlNedf)GJZ#i+_J#hFLF25X^?~B0jaDQw(ZMB#9J2t$y@Gh zQvCO#!!$4V8W+Lv18lvt5Wu9?CG7Xsy%YqXUaP~Jj5b!M<+nMqSu;gd)tG`G-;_p# zCCmpxkoZ;0;=%3ne1E4><Y}~x-Zx8(iQRXT?aNp}auD;P&#nXj-EQxQtF`S*r;5*@ z0kuDej$#i~rRTo7v^3m&rH+<Svdl&pW(stSs6w4f!C3E|eH5wrCTY@bFFN7lx)FO) zLeA*RuhFuY6zL=-p8?ci_3wN45|VP~@Yi`AJIspvDh+hIihtg&$CeG>)P!qns2v;} z>{zxjtwYkR9Az`Q4x`)eU3W~4XKK<{oDWFwDD$XKFBr$EJtS=JpSWY?ng>tJRhMcS z-3j<>OES}~M?2VYCyMCh8z)Tf`YP?u25;FD8@dO(wDQ@@t~b2=uRT6V5SLlW?LJM> zTarCiJjo_Ln17BKox6<XJi2_SgYMm4!t^u&*{-z__<e^li^H`b&lE4CBp%{JDin;` zp?0N{=cvP>FMzANA=Ute^`}2(aaGY$3E#C4V7ECbRe6_R?{Gh^MPNdHNP}>zw*2m) zkc(t4PLo(H*5Alf>=b?yo<$N=@eqXxf4Sbt%MbG1Mt^dWBZ}nYHGALYgc(wLdN`z) z%!T(&q%2>afZ{>#zsddX|HI_oBu<Xu6%p?I>i{InNN!@8lEmm&2Ys>hXpIa0^v)cn zTpM2TF#NK#33gf3pobc)Vbb*VCBGxx0$N(jzHFjPx|LR9cLfe;3tEk6LPs66jQf5+ zcIHSo|9>pq&-7Nzm3Q^TDnZVI+3<~FLML!y8Cf!$rJZhZlrC+aGwrB>!?h3I{K-q* zVx(oy@kxSB&l=>0RPfm|jq2_RF9J^cLD?O6XMPBX$8<l`{Iu{G20Jm3@(#V(6h`}Q zylQ${c{Gh1`K<Ly9uZMfCHWvQ^|iA}oSR|kiGL5TQo5FxN#1DYU><{TPD_GIux*|w z#z+PkS&~Ow0TEox<K@5|5j(|?8b5zJ<(g3R#c2wCN_G_lO`3igq#Qp2ykQ>n+T0{t zH`VfwLk%Q0CG))u2=%(|1*8eGg%XI8ePyH|`&wAlAdY}p9@X!iCpiLq-Je3%CM$UU zN`EYor7t`*da!`@KJbiWtDGT#|Gbovs0z`N84>nVDK75COZ3wS+;zSSH6ZUbx%Fao z>-~ZJdcg{YNL^7c07iBUg|nJF6?Pw2XXk_O@|I$V`7VvJi&Jl`0intJj_QG_+oYzQ znQn=(w*bHo5R0cOc6uYuj{QZE-JP~Zzkf}Cwmz|(WF9&Bi3S|9evk=Xpex!qFZ$OY z!GPKa=#n=ow@<Gjv21;-229jH%C|19`uC!lV18Uyj1r?^1|YQ$_sub9e?0WolINA3 zl!u1n4j?VRN{g+^eD=u^oEln}3AMOJ%c8WA#|~xCx*+XmB+ll!dFD3?S6~vN<$nq5 z_AA@uwBh3WW~;|8Hf|o0|1kgvsg03&&SUJr^+fmStv4VQ`)!Tf0bN~wXMap9052%$ zV9~Bbl2~YJ@Nzs<9nYg92L?(|LcFXD8oI5OE)$cq^luy-17mG&4p8B2(`?+i1%p(& z?WSd1QDMx99=O;X)eU9S;IUarSAXFy7wF})1?~iKH?dJsKcIec`*MhqQ8#sGIC$`$ z4bR?g7}b2tbvRw-ie)TC<fM>p>i6u&yH@p9l@*eIg}A8{lNh?j#3_SRiEqWo0cq6w z<V$p?X@z4)vEM>ylDH_AVq@weH>xgEk>dAkqOaOt7W;c{9~(|iw?=olKz~1KqMC>t z!FajfWV-k<Agg}X^NPV%-^?e!gwm+{Pg#c%b@$z+MV&PXkdCx0Kr@0s&sGTJo@<%v z0><#^*SMUSLCrOxh^Mq>EkWKDjkK7sCqKvT8E7$CC!Bl7V?^APC6;Li;O2vJZ@$(V zd^@GRtbntBLk%yQ4~6cKet+{jjVgFTJoFr7+3lj+)ky{u#C7BA>q>bBtpr%G!dO1l zo*3NWG9Xz7?c9?fpfz?Bmz{e*Q2DLyVZRWxZhE`FIjHWj80|;To3mfZJjhia$}iZs zk528KIE|jP2kta<sAUhSm&jdJtu^Sfvs3>GW9{V7n>SMv+wlVQs(<z0BYgfCh)k>- z(O{{%JNHS&JF9Ec5FQU(wl6$w?W^wTOZ){*;OD*#yIC8K<tGH)#|O3J5r|AA!UaWe z<sL@C)uN1#)Eie&=4*0|w-n2Fg5X{m*O>r+X$4JJ7~}_hQYJF;HLuSgnymubmVrVj zL{XomcTScEsa4M#T7L(RRedlIHhAscrZ_g**Srppp|oZi=i@e9C3mBtpmXTgCjiNG zT@si;5$`1@zq{VeYx$HH3MQ||`ghko5}EIQc6__0?P-+rGCB&Y75x0^3sTvw02Lug zV)trw=9yoD9#t-70zbzQ(dg?n4-gS33PJ^L$SKhEw$9Ga#(xki8uC;)@ZFQ-j-6bB zhMFifl@UMfu`}Hyj~wM#zc@iLk0(LoJlet;uO3m!BgSOq89(wW97G?)PBEi&$^DGS z^`x}@YA7WPHCETHXxZ6aPLNXcRLEU3e(l;ceDFcY9SKb3y@O@4m%3Jcq)AcLqBN98 z-cj7l<M=Vr*ncaez8=9z9$YClTcXLpSQ*6@7Og4pz>za{ev48V*z){l2YgN^FVg!W z4E6IXgxj)+pY2bu!u2!OWYRX-^PVEGDf^;8{kzJHtr#e#5{VXDP`L?U8Tt#0<M>mq zZK<5(0-qaq&$a6S83P^E70rk0omu_-pOWJmcM-aM+JC~XwBkVkaSvSzAwDr6C0Co% zZt?TcGTASoiDD;3=V(_e)c0&V#GaoV*&Hl<pvKgqvKR2zvp4!&D7F=N*7#7DWI)4? z&319iUXqTd(J55c{PE=b#oggJ0@t&W_YOUN37Pm-s4!Ro{|ZEJU~w+(SlF>Y>(TdK z(Pr&+DSuN%Eous;sSCxc0+o`vkl+uQ`;&M%sN>o|JjK{9BceNQ9>*r5vddvXT5jJ! zT={1h{ytZq@utyGGF|TEpIJOei8S5cIf}ht6c}S6SIngi!5sz2S8`buR{xkdYUH%K zs=N1*#97PQu=sgATLV(B^dQh7P7N}((j+x?mw)tfJMf9!va)Z<jYwA~a)?Er`o9d( z0~b)V9=c5KOINS1_2;m;BQ5=Lq2<xemZKZE-~;h%EqG%s+y?oHN0_gB_6*wHj{3B- z-w1gUZ!#G=A}(EzuCR}Flp+mq?>4D>oHXkY%^_Jq2`yAO9czK;%&*9+^xnZ)V99UG zx_@Gov;bwyeBrHEv&&M^gPCrr%h4}M>g~PfcQ5BDNP0lb%N07{yyxEDKkeBn?f>L2 z%7`1UFHcr$4+WN0B<#pHQAH$7HqZr`Um|l<C3@-=#zd}rp8!bcs6WIaqkIKfX88mA z?Com<t${2UCWO@c3%nX{1hPsZ!+nDbO@FklS^1N;DP^=@y<h!$FbrmBf92|_KD2%% z`lA1W6R(+>y|5F`)9#MC<r2%jb=+ZKehz;*W;E5j@(WroymIn_GTl68#-<na!yA(~ ztb_a`DFOW*Wp)8~K06ba2}!NL!W0RCd#LC)9Bgh$@!>RS(ozK(y=Mxt5{{-B4S&KZ z><)SP9C-ShxJsA*dl<ZThf)|)aHwUVe0zsJc&A|SC?)ty4#7ev7Kb6CmpOu;*<$Vm z!VSp^^6i*T)G-9pJMjPlF77b61Gexi(7&(qcn(~o4U4&iNXgc>ecvpe5v$S?yB;7k z$f3#NB$Szs()W3r&(G$AMEbTmj(?$$5Dw)B;mz;xc*%dw8e{r(VT?39_+Y~8O`l0z z$kQ(NbJXT4LnErv{u*b1U{2te_St!h8YAZ+TXM}R5E@={1XdBs)&mY{vWK<I78-#Z z`jwNdZ^5CGK^H(u!Z7#0(s=)w26z4?Mrd|8a|^%ZkXzpLTyRliZ~uA;TYu`^UAiYy z$j&`lT8+CxFOK=xB3gEP;VPgDy=;Ko-dLUBjQisSI+-AVUNEhpS+R+6z$#Q3?j{Qz z=;B%w{Y^uV7LHufrg!uNI^ihl{CsfCU)RH-^w>%;t(Ik*GdWK$TAl*xMugZ1ic$E2 zhUor`$eN!UHg4Cu7nfi{x__sCwWT_qxG$hFicW6EDtg~z%#No$pFA>mFqe8FdSF>B zyfbDWUjOupxSR(wY=^c*Bhj1St`Y2awb?dRC};G1Gw6`c;wsWlKmuu~Nxs%((sj?S zaf&Q>dfHfkjX=qAMMF+#k9tE$j>3jFC6Al80wmp_0+ER2PH1ARZhyJ@#(p(d!D8Nh zst*aO8cxG{^rF@6G%7pLta|vhlJ{BupU^{-taF@s+1&Yb1()oxte;!6gQkW82LC0T z$AmYaa*8z=9wf&X&>!(Jz>m9Ah>?_Lc-ITGiGzKI%=-2*erab{hTom#bE6&3plP(D z2tgCza!i<<x51o!k$)+&Q8W_SUMGS`5NXpPquf3_byEh*mDKw)hgvwar0HCworZtJ z;x9~)YtBbrVDFyzcoRrMTxuZco`~QE!Df~2*4#=up_Gewlduk;Yy)R-yCT26dU|JQ zvcqRQJwx4si%&KwT@}a2mPD4fLBYq@?s<Cus2J>eN5mCmsDHv1`=O?fJuQ|@m%e`3 zjsIa)IX#GA6OeaMcMl<sGh$Y6|GLQEgxJO;J>^jYni`@FXK=Jr!;okd#N+R9>zyxD zZqNlf)NggH?NGpD%GXZXX*7N^l<abXDouA4^4Y&An__~&4YZ=s7WD7$rN)u_>00Th zl-@zOP9n?QW`9V_bW%L^rpqu?K<r7f%8_DTQJL+zv0E@$S|FKVA+E5{dlWs>7}fcD z7Vo_3z_${ZO~lVi_Xe9(^A3;vmo6z)HjM29yf4|U;b-2CR_B@jEp+cE-qAjnWKV*O zIk%%<m3b{zE|D&@rvBiGfz|%-K5q!x$H$3~T5vZ4c7K-h1C|wuDWhC2XHmLVm(6~= zk;1~B^G-BB5vmbcf^La_*Hv(g>?nMyiz~V2hfP||v?k=*>9x*mB3L)l8<Nk<>2$YP zoc|EO+N7Ds**Z|bdp%_mUCvh60r~V#nk!q%E7W45^7i(QbyYfWL(D}r{~yHeU&sEz z?y6Hi4}YJCL3Z7>v-?&PyU*qKnrEW}p8vcaL;SGtlB&0*Cb@a<ani6bS)F70<=7;u zzIhWHyPgbgl@RPrR?)#BE~I9;y6tnks`0L@LzwaPQ~`<F1kpdc*t3S#zDWC{bUga* zi?3(yYZJbr7^}{|0sB@iedA2+dlS7oW}TZOTYvG!=UovGmc$#R%U3TPABmsWFx*BT z+hVDX3V>}itxMnQ<UUVG%haulEDkQjBqvJ8NPU%{EFCFxr}5F?oUf@IU!$zkzflRl zH}p7+tE!bm@c6A8xhHSr?Zi#tq~lMB&PNB3N@irOpCIj>#<MV_7rGa^92bnla>*gw z&41wc*(5x_-6+7&lf-YEZ)9W5!ogEYeS_sXrLi;37tGV6zUFQ(XMj^!cT`U5dUis= zl^oVo7BArHdi&{V_CaIj;^pE@^pYlrLNm9bK#T!a8cj^mQ!%HwxFWGpT1Lw=#5>Hp zq#&`>dp%^aDL44m3|kZSjjA;NCFa$qqkom;?5F9e)~RO;H*5$O&&)8$^_0O(h~4(I z#cVty$t_4WDioFJ)d}iW>sB!=w{03IU*TWDFVtz}8y26^y5Y1HXC!Yh@?H{`04g^Q z#L9Y$cw?a#%=mNXE3NCH3ftfb*yGn@$SuavTqxH@)A0qCE1Dx(C`qxH5LlfaBY%W_ zC_}rpNN<wBn^5p$*ik?KLPo;AL>{bCz3b|1QqN@kcifx)m^PiqhNB418tPP1Nr6+% zb}ZP7>(a87v<M$8&jluJECO?c-pSHBw87=AYovVDTYn|yOtjLc#n>3(TK@MF!!TR~ z{Y%XPi4Tm#-YUoF0av3XshJz{!+)*zJseL$dNFYq{3myYI<Vi1#!hiG+RXyqwUOyo z(fa!Np=wu^TjmSVU)H6|l6Ze`a1C^dEhxDfZk4!)dNO?B@Nia0pK0^#>%}pdV7}u< zjkh9g)VO_+4v*|Kbo|spQ&!>X=aax1sq5^8_!~g2|1C@`9W#)+D7&PtP=6_D(B}ld zMz7p4TTs&Z`g9R+mbc!J!h`a_*?y#hEm@voaiIw%$UFDgkSL!*AOk$@iBep@Obn(e zvyQuH7HCBTGSbOCxnLF~do}|(xjmc^esiC&J}tClG(Us7xR6VhtK4Fo)msO}3@UAN zRd(fI@Ey?veJ$Wo?%sJh-GAVRFFo4UU($=gN=crmey;m0Cy_$hop;08x^RjePS}Xf zxPFC7+1Vh5a!8c+ixo9Nu_GUT<)hYC6k=Y@?G&g!iYdBOKIYrigJf3K;1<C&Xo0_I z-|<g$m<jm6+`6xBZcfvJ?dn&ifi`@oKOPa=>`&AXFNmmrE0Kk}Tz`}61N$S+EY2d? z;qfCF$|qNu-JQmommRu*#;5ytVeu}%bbk^)kkD$Gvq%p~AOh>Al-2@rMx&Wxxm&+w z<iFIoStLc1-u`Mdq*NQt%!N=IiK^}p%i4**iAulYr@f5}vImm4#d7_?MMAa;?@?H9 zC3jr%u44!oL`FxFPk;R)S&2h{7TlF6^RhrFYIepKoW71A$KW7T%JLaR9kTEms;?+j zYpi5*@R@;n-H6kkw8>E_axLiGT)kNW`Afyg$DQ4HHb47xpOTur<Te<aub%2-%3wC5 zH#q7lBA#@RI)3N_v1_5+BleKP@C>tfWR6-JyOF29c?_ZXzJKC_70fbGWIv;&Ay;2f zJmmMsveID;@BJHbbjvFkna6pDGe)o<pfl+JyB1Yl2A75W84iIrZ>n}4ixRU&*T*Z~ z#cP%iBN)%Gx_Z9YwV>Q@)aBNgC3XGXN7vnkSvU^jw$#V9gxgx45D^3P>eeA%RXBnO zTIa*5W>jy{j(`4dFQwKF>(Tw=KK0e%J=3dLh7H=hGht0?aU~x?&-HNFAoMv+gqU5y z((_L)XI2Ws$o4b1k~uAEC$-CT1pNpr*8|{I=B8XqfDfG?&QVO*P_;9395kX>XDCT) z${WL1hRa|MT3u2N&JI+BCKt<o_#YD*Pm9Zs-~eTTw|}v5DDyY&M<!QWrJ(ka9XZjQ z$Ac4%$x9QtoH-+`OvCY)wc9q-Cv2O=wZX&*(h9~z<o}81A-$ER4SQw^36??$S5<-I zWtaVz)~x7y6%`766YBXh9wgO?+%MofMVwG_uP3p35<;8MV&6)+m3B1`Y!Q{S%s?xn zBg4)b0e@u@$1b8pJSqgPND;;s9kT*rl<BfEG0}P_3z17Xzg%K^_^w=4#<nLt^2<ps zGTs{hM$`)ul9;%|yiI6)dqc!F96CXeY;!gxwYyr$&CxkQiSzIQqm`ofp%*kEdZq}@ zsZn?JmLLPr@I6Q79G2X{Kz?FuxbE-Y=Zt4x!GFjk+8>WkI)eNhnGL{jW!Shn6cLry zMW!r2A-|=k$ABzrM9)-MqqfMlGW#Cit;D@iMTMy-GhvIL69s=e^DB+;X9)z=;C!{D zH267%6sj~<!R8S!*T2$#bMRxA$R)otu9nxmHAPP;bb5<E)5;%9-!>?gtIe~G67%Pu zB!Bum9ff6bPr0N5X+mmy^r%pXJhqU$@Ej=@KWHvyBvF%H*9{G}$|ga)Gm+E?x*Po) zeaE$8u0vL<vGWVq;c%{E{DEg6im~qFj3>?!dN3U{b9Tyb+Olqk3x4m-?2YE*WadLn zWW10Zb6BImxNLznE|EelD|nk((4OcDNPni@KTzoix-Og9``Q-&2-<?uj7o6v+I!T{ z^J%|yMH#$zb4ZjhBH*Robo@62af5@J&ic9gqABS*mvKkvBoemB#<a~B>S%OxI30%l zFcZ7l;<elonSSVzu1csiA5I9fhc`iiJCa_ZEJ(C|J)qfWNdVsirAzXm$#%Vegnzk% z;b*#HYlG`nZ-{CI_61q;nCWb>k|+`Nw9i%m950h;>v^VL?5OaI<bEMzxj_k{Qc4G@ z1#Uakhqim_o;*gg`S}YdHhz||2&83~y!^sV88lR#Y+U6g(XuLzI!lSex$R^gBUzOZ zQ`)|qlhW9mEwyj8TZ-yT8Qtuex_@c#5s|Ym`%s!yNGJEBH#PF?ypOhI*{emkCE7*= zCKUKZ|1Ki`EsG}g^Ht8uW}&UH;Lf~HF!0*L8PXS)I|D4#YWSmV?H_GdbL*M&C$#zX zZo|~rzv%(@i&h~`2`VhVv6DLCJN6gCmTy)Z3<zj{C@+0Zv-k^3oS=Ts<$oiX^)g)X z_x8&qAFQ9Z4F1MF|2WkOn*R2J<8s3a%U|i}pHVJw{zjbttg{Vl^~F)kiKY$CU%B)< z{0=vSzjIT8CaA^xn-v#E?(n~Y(_ayK0-yg5lry#dXzF1DO2qH~tFoQ_%tRmm4#X!I z|FLJ2&Isb)tM~_R1jw2_ZhsE178z{N|27w0m=B-eFa~Q~gp&H1nVG8?KK6e4J6L{h zgR)s~&oIHfw2Jx>7z_XR#T>NxW2S^v5C7f2_tG9@`(uY;ws>unzkej)Lf+Y5bv#eb zfc$rvL{$B4^~9VV-~V9p&%e`o9S^b6<~d_RBJ_X<_aFYlm?VnXUw_`~uucQ1*lI3h zunG14?ooqCP-|}6h&#gx^a(U%^4c==)fe=Pd|rQVi?BgV9Zju3q*|_l8UTms>o%tJ zOs`R$9ge;*4QKr<9X9Uhd_p9j6&XMqwrtE+phTuK)8_TYC*Mb5s)+aEFkP-!<P<}V zSfbV<)&{2iv++~d6MxioA|H9kYmW-2j*#2jocn$_l@G`<3-!z$)|0+C+&pUh`Hg1( z^?%NRMDWAbE8rWt<0Hpuev<CKhr`O!z$Cph+)&{Z<cMua)eh@lUE9y&VPMGBmugKm z&(4_N+&w+X1$&ZE0wVMjN<@uohLR>n2B6#9+lPg-Jd)dwT7OTd-Z;wS)m$zy5sq6K zM-C%}n{dw_9y=!{6dD1ly@;L(g-7>|e%xM=RK12s9;+$KEyFEqi&$H0u?~|3>-|*d zLcJP*EKP#K?)^iq;n9y~o4M#yJ&9xm@dBT$NFK~2YkD$XpR9Pm5~k#~ro*U^Q~jWJ z!wua^)2@_vG=EOpl5!RC90;mIGd7rQ_C(j|I#I<GecTqGHbshyLd&1y64J+SQltWS zI3sn?&$g~2jwlK!+Ey|1_enh%?2`V8GB7D_X1)D|SvT(-LDFJa!TSNAWxMQFGn}=@ zqr4zxWNEtWVBYXt%7-<HYgGeX1b)gkv?WlBK>qO#SAWmUr5JEzl*QL${~ZCQ&22z) z_eX27-{W1av#e$QCe8nexAPqJ=8wP#p15ha*N341X9Wok!NjZzZVC}ggyE4|Hm2mW zcPQ9NiG?X5O6YgpRfkDjf~xdMnjP~%n=tZ>67|0F8Le0&^*qB#i8OhOvVB^j;-gVJ z5a-U2tbcCA7EkD(v)#=9%V82_7HU^OvS}Uf%ygwzlf1`))8Q=bj7b6AuN@FPGJ^mf z>`!vSfvoP}=(|lvci7yHVoRCI@&g<L4sh<yZ^)~6G7VeDqpwN8XmMnnF-QRt@o*)I zB?>u>`w%1CQJ`2yFED}`KtB{O208GJl~*f>_kSC9M)N~J7%oi*$~(s5>Lg0$mQ+#W zTDf#qT~Iicb3+$|zxBsjj$QOj-taW!<`yupj~2?{ESeA(5}*2Ys}?QwA@&Je4GTF% zYf&QWh=r<~9Sh81RN=;<@HUeLiq&_fCidBzTRRdQ)M1%<s)|2H-}gMcdnbtTPcMKP zRDW{<;QN8d`T@UU+TsNOwwGYB{ID%Pc?k6s&yI{&>oZB>6Og_k_~`Jld>~MdwNVa7 zNXO{|@grzsG1<essT!~SOzE>0$S06{|9AkbDNbtBIp<40b$batXbV*6uYaCZ@8vFF zOFSX=l_shTj(A2Jdm{YW7J=xzm4;4!NPoXXDM4{CNc_`|;xX$W!pLkn@5>5mE8#Qm zvwW2(W$6Uo^|Y$|Vr1jTN~6uY{SCd1H<p<q*u=+|Ur_=`7X3dD(k3)_jD8*t5rb~f zWlNiJ-Za(GKeNd{azbF%&yY7bSM7~$lSqGqwa(bx?)QEvtrq3eSVcDEAK@(C@qf#P z@kBlLjRxziV`a?i_DwO<EiP{#`i8C+ZkT5i-=x3}1SLsUF%q7?crsfIxL(%o7vvSE za87#LTLi}#lPLkVM@ccWjEO&V$02hJ6sE53JS(UlUsYXsQMWHUFpXsadB-hOiZ*cm z?aU}p2d|9ITQ4!iJEkoIVZ81Kq<;*YSUDqi8(TWE6m{z=>?{MC&Qc3roJ#jfOrE+W zS}DgKHKo`bH#Vh5LN9fYsnY9P(Oq3K8=Od2^qc|SJ-NtLe<2y~Y6E|r^`@je0^Y;b zuL~|rNz=RVU&Zo;Lw`7bH{%^DE}pOYMqdMoAHSi@OpEHl<Tr@q4dLN!{C|=$FLoyw zH)}Chv$c4k>tQpcP9uDe)5|V`*Eh5e^CUOlBD<@-C~!kryI(U8u>>B>+Q{(s4s2#J z<<6KC?(BB_IjoHpJ|In8LxPKasAh)mx;=d;DJb-Z<7qX3P8NJLS=SB*yT8M1;)y8z zSoRke2HT9$+{18+JNY%%!+%z8o>OB%x|G-2HaCMcz0jXXgS8*NF3{<!?JxWcmUbeN zeWPpdkpA&0H%+%ly1iI^0$U`0ZGB&*T!UA&5FQQ<!lfaQu5EDd&f!a&(IKT?g4U=Z zm^a8WoA!O<+1JnH`G2tY*0FW{TH9cnq~V5{nVC6hn3=I*IALaL7=IgPW;_jZ(l9eK zGczaC{_Z#Ly`S{<m1Z=5%t*7OBX4_K-d@|w%l3XA`#40XE8Y%2C^V`wXsXRUxN^El zxj)y(1v7l@tJk1%!=WdNjgB`WVdQhiG_5Z$Sv?g2ZP65iN9cO;We@`^bNz*G8N(z% zKV}o*;Y7c8e-@bU`hOWkm9ZoE$3wu-C0PO~Nt^fZ&Zur#pB>%_NtvNsu_bf0StUK? z$3;+`mbqXD@`WJ_mTJEfFq&uR{@M^hPVCz3WGWg?|GJ~3tpACTYaf+3u2aa&*8rTk z8#oivB)}HFY#XTE93s&KbW2S|mPBt^f!&#d=)}HE_f#wbqJJY3f6>p=k+;>~ZAy`# zikysw#4@wYRlBn#)wg~c0$5$i-98n4DJ51hsRp@q5Qy=f3TR?GEDj5MhPUzV|0U2l zNf~CLK_$bD2HWli2$R1qrTDmF>ih<Em#ko=!-w-t@A3<7lx*PoZx?Pot`6_m=oN!z z^i1+pHQXcbSbw27zFI{oDV0a1Nhv63xDA$48Qu_?E5O=tJckm@fb{k4_#LFNu%iIQ zcFgRFc4}K2m#-13eYi+r!(nh79C-rxwNQ%H7UCrrc`T-Q7c=kT)YEygT3!2ydd+7| z_1~O;4z!RTt~t!boW*0~XJ$+Pj0H;}f(A`f-8mYeKYu<XelC8|H?+RhFBO@cb)PFW z&%IuG7^dPTJ&GeT0&b&4+>v%MUr4{jRaNo<=925~&L53n={#!l2yCOPPq6cbcMl6o zcD1ve)b{kYFIJ*?hZA4?O?7t-*xLG3bXI0%_Z{gGv^-q5J10|b-2qzFgK9p%49Z@a zA?{S2E`Mho$cv_dbKHg4$&-BuvdiH%`f7dW=N*q49xGH-dFs#kr%uxblILBw!GWEf zZtsW_WEjSvQ$8Dwo-;SW1bnqlMI78EX9+I-^T>zlBcJ2nysR7sY)(FH!0gZhf$Kdo z8{sKj8SC7LEdrknP%A{d4d<Rjc~*c+q}lpaBY&vmr{@~z>-jzjW&x^vEv_Sg2JSWG zdhLr;YLd}}@JEK<FN*=x-)#AYhK9#{xz2(S6Yg-e*-;HWy@j?B1n&AUtoDi=H55Mp z$sJ>Jx|w@AXJ)7vay%Hf7cd~uRU%Nl{kXJme5#-p0Jp0<XOKWaOrOyQKy7R{BBSM= z1Air<nTZOtJZj{H84GWgib=46<?nv4HB(GM{oci&X}`zMx4GvclZ;sp;(v>6P#+;| ze-%GI$nu6bK8LX`lde#C1QDon43-Z-es4OA3RS+Lg(#UFLt?7d4%smbV%VaKR4FpX z)JZ04nie6QWOM9F(&(^eO^eivK^tH&xPOX$DEvgqbx$}NlV&)T8n#Pk+MmkA#$@A5 z8?cPFP!(w7W)N@4XaVcKnc~y0+k@9S)pZ)(ZxS)=m@iJ1K<ry(KEoH{AQQ%A=c|fI zJ=Q_Iqi@5Djziu~(msic9F7vt_%rq@5FX7)mg^DyU6dTaYlPSRruEA;94i<ctbZt$ zBQf^a({s&>mV^7F1$^xnq@2i8SZm>w6guGtXa7M}GaO6OzT))ZU^Zel$^!vZn9gA~ zk_nG$V4m2b^qjPjR9hI2F-Q%B{Kfv|n)ujgKAx@Y18Mg1tK|B_d&H2LZLXu_vA&0i z^GX#j<wjzE1T|+Qi7Ibo(3Wmozkh9Ae5#~bh3fDJc5u}zT)oz_K{!t(>xNB8XoPX3 zGyE;}{;=eY{rjuwY0D~TYyPkj3{$Z4w(1j~R8oGe=RH$NOMl2xC<xXtq$_63)oqJA z%hu<n$}ef7&3-f_ff$<?O`4{la$}WLod?w?cn3U82QIkQz56AO4$<LJr+@zY1Q-3O zb5YE^9Hu15v@rin(z7YPHAR+OO!Nq@KMfVMwIr92nmG7yDX1SLM#M?67&iJuTX30e zk7}mHOKza)orUy~r9Fhqv<WzzFx62Xk>6=`x(5Hc;HBdiw>(;leSC_?GH*B}?T@&& z2%0r^Ql+kbC*zyT*uE&K^?#A)1{{*CwTJ;fP#1_ntFIzvp9gI4+C8VX$DyqFv4C{m zD17LuebFT?q+RmcE~5UC>ZQA+Bzd(yc*WCHqU-=dlIfo#Uw$r4-~~b6_4qY}*mYWX z?oznW^n8SbwGRLAu;^Fc5H}#Mgu^~`mL~AkdL;8^;kaTX)#(y%&3{7_nZOoOmx_Fb z5KzUGfi1@$oZ%x7%V-g8i`ZNYH)7o7mR*U36Rv$#|H-+BS8;I|9Yit#cDpY&TFs`E zS6RG`G<!>itDO-V-Fm#~DvWuyWGYx{n0$5SPNMTskJcIO7Lk?cvi%TFC(&5kH+nPA zEeTQVGBCr(eaDihwtrD%YAV{B{byzU0j}pZ!QhOM&Fw7Z{X83!AEWzq9Os15e3B3j z8#CB37#zwr<z{6(cMQXs4gzIM6TDZ{7eP3hCWUSq9y|KT$w<RnCPg65z-JNu{n`}V z&8;oql@AmxKf0N*tob9jR;9?1o1+tgGTBi8Ic~@9G=s**Du07&4=SA(1tPhxNvNE= zNCXV~i*0(*mNe#+Q1D*Yu<d%R!+KPw42%#REw=}}*$~KsbvfT*d$f%kF=imq1hPak zCFXUsR|zW9c_GY>>y~68*7-NdOP)vh4^<~4Rn)kFHAFW4XE^-X)8Ks4nWfs7OL{6n z?eRiAi`fc2G=D6-W?a`Ha}0iN+^&7pQEeZYhM~1|HCV|ZrH~TalW&9h_8Nj5p3ZjB zu5@bfcdt?<@w~1B{JP2A-j6&E^*|*BN*65LV*CU91SF7WqceQ?s*Fg;C`f}_KmLNi z%AS_DMkNdjC*e7ABJ*`eTuU_mNcYyfL{^WhQt8bUD}T9Ph_28ZGkN{vnqYgc?CxT$ zItv-`dPzgBdVM)^{toNNV|G~=+vC<>{n^*7996=8AzTeN&oBcWm#w1x`#epden4JS z?P;{RF`-*W)od_iV?AjqkpE!+l-p&kU5_lCRMG3D<;9);Z3YI9`S_%5-k4lOJWIHd zA)ycUXn&6XQ1(@>FU<Yp@H>OzMqYe#Qb~+Wa72@wEr$8Y+K~CHwf^ZVBO+d~^)HiY z@1XTFL?@^Dx?_hyAU}~&eAfDzTBSh8nJYG5i~O%a>lv#D0<u;`eYr%R%3|eyvGHKZ z)tM4Tj*TMoV{ZK7!Sp3WM^tV6+k9(ckqgqT{(ov`sC=_1|L^F8g>9n1!UaMCR(u;K zFK7!?h8p%>{lFSo{$F#>Q%(eL80Z`C;4Baj-=2;%Nc-{o^0j2mm5(9Z_AE(#*w#dM zMZRrEP9Qd?+zS@Hg$CJIC^5OtzVZ^V>qm=kH_JX&-WkIIy%0BMd@8F4?umcK%a?JR z^naDP2)JyNT@Tm%v*C~d{L}qwjgzCGpr6;<mBmBLVkhNm6~eq5wmr`i$E+0#3K(dF z1X-gzK?{+4(`H~2;_lvci4estmV)T2BZ!oh(@oCz5j#5dX_x#Y%Rs5*AGYt!)cEWW z!Zk(Abi4!0x2NB=cFBZ4&-mVklUKg?PJf#%bw@+sNqvLu{-#*IqD9MV2a?!J<BYn> z%d1f2iIPy0@e=G6sm`HYgX*F}u__&&{FsmvT7lV6VLyC;S5c-8xzSOJD{gTo31MWD zK+=;{$;ym<9^DaC7LA2B&I|LDGq7?;3(?mNg{1<IgQrv?w^}l0dX<-l95<BXDu1W3 zGWcTUwx~zT?W5Nrgz42m>j7)qt_Qi7WmATXcxNr`r2Wi9jL_(E`8*w=elW-F^57Nt z(WF5}Tqxg^r|u|In%qCOh7t5Z!P}pJ=xYK=PxwRcZYH|1Xk=J^!LXj`V3q4>xO7Uw z)A?yWFg<-5ztTP-w&kur&F#)*@qZrSvZwEX?8PxL{8#b2&25v6XDt`14#@g#KAhk0 zrQI72f7bxg{B*?hVIPn2e!N+bpcEp1&QncFLNd7~v&}xm=Z#ZEI3SmdZS+lMivFHq zt0@oOj3xP&1o<U6xK7mh<t^)={WJIy7c9!)Jb2vL9;3&(2-JoJAe7&nPk+h#__(c% z)#>n*i5xghlgL=mu`NYy8eX)X8Pg#62xVsByJWPoPh9to3Af~cotxCB28c@I24FjD z-dK1aAFT;>b$Y5=<z68h`n<tXY7P>$da`}t>KhxA>~t9<l|P@HM^a}rNEVGLqGN3F zc}Et*4ecM{FwI?^l~oep=6`>syOn?$DC+3hwBQU&;lFRqan*G7tX5-Sj-be*u|82E z^WY*TLAi7&HYiyrKz;UVLyO&$BP_7ySiT@;@JQr|83b$nS@?}dWFA)VhF|z6+qE+) zoXuC;x=Cd3Zwxvq_K2}h2zf>mjBR-BI0Ovq!Py9VVk^#ByZ6o-SAROn?jNNq)@~Uc zV~8xCj!&V`jQ$59e)%tbH#1dmjUsKwlyLgBrei|qtG70r%20LRM7l)QwRtR*AARaR zrUes7cH$ORob5Bzq(=L~b1~m;exCYo&g8MP2VOL+&JJmZUt`=<hGbwY5J!8ph%6f# z=h&lh=pvn$ROm`Y_J8ZQoE)r1&Ri!$`afPIZ(sK$T{_VPHv&@41p>9DnpGNbnmM$` zXiMiH3l9TlMik0laAg)-KELBjlICyE?WuJW2rwK0R@4t3+o~BospV6y)G?-LQVM3Z z1HMj7jimXxz-c_`Sx1&~;qe?nf*Rw21~;Q}v+{jkzbz|<x_>I>*t-5cPAMsK7hp!G zPirk8zeFMXdj0JY&G0ZhkfzSvTcl2yeZlbET$09cgf`3Ipuq=hMx1}pc1fC=zDsED z9gmrhTCC}D25HfDNtiDDUh0HB>Rx4K{R<G9UdR;2<H|6my0XCU5OSH}G+r&`uDZrI zo#>o&pB(k80)O<j6hyPGHo~OFl)UmN5D(cbmZ$lfJWaIrfX={xe_t*JkU@MOh-u;Y zM7i8*vpn)2FuC(_6dQ>*2OO8pR@JIH!mTjMKS22zzTCxf&`1DSlz$562?*;J?w!7r zi9F`rwj`W0!HIOAdJW1a$LjwbD6gnZPf5i4?$L2WsecLEA2oCK;w38Ab#XgV8$_o$ zYk?=d#X^2r06tiv1*AHY$B=n(Oj3=SrJ#$sVe&r}CtPx{m6NVvb=FIrg32cESQOND z8Ej5DU!&;;j_`Gk;@c8aE)ZLBN%xb$giRqm&g*=oAGSPMyLA!O!EHZ�Fi79%!UP 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 Yj>kZ%s!F@QQRBL1&H?W_z2Dl>r_2(Sk(L=h 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 Yb!Qub%#eP$-w#5qE=wswnWIAe518VBxBvhE diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip index 6a8a12b2f2d494b7fbf810377e0b3253c55ee53d..a1c396370a937e048e1f895f55c3d282717899c3 100644 GIT binary patch delta 3298 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 delta 3153 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 From 65cbe4c393c2b617377b22376f68c7d9ae9e691a Mon Sep 17 00:00:00 2001 From: Thomas Neirynck <thomas@elastic.co> Date: Wed, 2 Dec 2020 18:44:17 -0500 Subject: [PATCH 079/107] [Maps] Always initialize routes on server-startup (#84806) --- x-pack/plugins/maps/server/plugin.ts | 24 +++++++---------- x-pack/plugins/maps/server/routes.js | 40 ++++++++++++++++------------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index a79e5353048c8..f3241b79759a1 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -147,27 +147,23 @@ export class MapsPlugin implements Plugin { return; } - let routesInitialized = false; let isEnterprisePlus = false; + let lastLicenseId: string | undefined; const emsSettings = new EMSSettings(mapsLegacyConfig, () => isEnterprisePlus); licensing.license$.subscribe((license: ILicense) => { - const basic = license.check(APP_ID, 'basic'); - const enterprise = license.check(APP_ID, 'enterprise'); isEnterprisePlus = enterprise.state === 'valid'; - - if (basic.state === 'valid' && !routesInitialized) { - routesInitialized = true; - initRoutes( - core.http.createRouter(), - license.uid, - emsSettings, - this.kibanaVersion, - this._logger - ); - } + lastLicenseId = license.uid; }); + initRoutes( + core.http.createRouter(), + () => lastLicenseId, + emsSettings, + this.kibanaVersion, + this._logger + ); + this._initHomeData(home, core.http.basePath.prepend, emsSettings); features.registerKibanaFeature({ diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 49d646f9a4e6d..d98259540f5e4 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -52,26 +52,32 @@ const EMPTY_EMS_CLIENT = { addQueryParams() {}, }; -export function initRoutes(router, licenseUid, emsSettings, kbnVersion, logger) { +export function initRoutes(router, getLicenseId, emsSettings, kbnVersion, logger) { let emsClient; - - if (emsSettings.isIncludeElasticMapsService()) { - emsClient = new EMSClient({ - language: i18n.getLocale(), - appVersion: kbnVersion, - appName: EMS_APP_NAME, - fileApiUrl: emsSettings.getEMSFileApiUrl(), - tileApiUrl: emsSettings.getEMSTileApiUrl(), - landingPageUrl: emsSettings.getEMSLandingPageUrl(), - fetchFunction: fetch, - }); - emsClient.addQueryParams({ license: licenseUid }); - } else { - emsClient = EMPTY_EMS_CLIENT; - } + let lastLicenseId; function getEMSClient() { - return emsSettings.isEMSEnabled() ? emsClient : EMPTY_EMS_CLIENT; + const currentLicenseId = getLicenseId(); + if (emsClient && emsSettings.isEMSEnabled() && lastLicenseId === currentLicenseId) { + return emsClient; + } + + lastLicenseId = currentLicenseId; + if (emsSettings.isIncludeElasticMapsService()) { + emsClient = new EMSClient({ + language: i18n.getLocale(), + appVersion: kbnVersion, + appName: EMS_APP_NAME, + fileApiUrl: emsSettings.getEMSFileApiUrl(), + tileApiUrl: emsSettings.getEMSTileApiUrl(), + landingPageUrl: emsSettings.getEMSLandingPageUrl(), + fetchFunction: fetch, + }); + emsClient.addQueryParams({ license: currentLicenseId }); + return emsClient; + } else { + return EMPTY_EMS_CLIENT; + } } router.get( From 401047e9b13aa2360211a24b893b47eb284b7b3c Mon Sep 17 00:00:00 2001 From: Tyler Smalley <tyler.smalley@elastic.co> Date: Wed, 2 Dec 2020 16:55:58 -0800 Subject: [PATCH 080/107] [APM] Removes react-sticky dependency in favor of using CSS (#84589) Closes #84521 Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co> --- package.json | 2 - .../WaterfallContainer/Waterfall/index.tsx | 5 +- .../shared/charts/Timeline/Timeline.test.tsx | 14 +-- .../shared/charts/Timeline/TimelineAxis.tsx | 95 +++++++++---------- .../__snapshots__/Timeline.test.tsx.snap | 19 ++-- yarn.lock | 17 +--- 6 files changed, 55 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index 77368f5caa7ee..e50b8516d9c29 100644 --- a/package.json +++ b/package.json @@ -525,7 +525,6 @@ "@types/react-resize-detector": "^4.0.1", "@types/react-router": "^5.1.7", "@types/react-router-dom": "^5.1.5", - "@types/react-sticky": "^6.0.3", "@types/react-test-renderer": "^16.9.1", "@types/react-virtualized": "^9.18.7", "@types/read-pkg": "^4.0.0", @@ -782,7 +781,6 @@ "react-router-redux": "^4.0.8", "react-shortcuts": "^2.0.0", "react-sizeme": "^2.3.6", - "react-sticky": "^6.0.3", "react-syntax-highlighter": "^5.7.0", "react-test-renderer": "^16.12.0", "react-tiny-virtual-list": "^2.2.0", diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 3bf4807877428..2806b8e989ee6 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; import { px } from '../../../../../../style/variables'; import { Timeline } from '../../../../../shared/charts/Timeline'; @@ -128,7 +127,7 @@ export function Waterfall({ })} /> )} - <StickyContainer> + <div> <div style={{ display: 'flex' }}> <EuiButtonEmpty style={{ zIndex: 3, position: 'absolute' }} @@ -147,7 +146,7 @@ export function Waterfall({ <WaterfallItemsContainer paddingTop={TIMELINE_MARGINS.top}> {renderItems(waterfall.childrenByParentId)} </WaterfallItemsContainer> - </StickyContainer> + </div> <WaterfallFlyout waterfallItemId={waterfallItemId} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx index ec9f887916f5e..5069bf6fe19a7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { StickyContainer } from 'react-sticky'; import { disableConsoleWarning, mountWithTheme, @@ -61,11 +60,7 @@ describe('Timeline', () => { ], }; - const wrapper = mountWithTheme( - <StickyContainer> - <Timeline {...props} /> - </StickyContainer> - ); + const wrapper = mountWithTheme(<Timeline {...props} />); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -84,12 +79,7 @@ describe('Timeline', () => { }, }; - const mountTimeline = () => - mountWithTheme( - <StickyContainer> - <Timeline {...props} /> - </StickyContainer> - ); + const mountTimeline = () => mountWithTheme(<Timeline {...props} />); expect(mountTimeline).not.toThrow(); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx index dcdfee22e3cfc..904917f2f9792 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx @@ -6,7 +6,6 @@ import React, { ReactNode } from 'react'; import { inRange } from 'lodash'; -import { Sticky } from 'react-sticky'; import { XAxis, XYPlot } from 'react-vis'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; @@ -54,57 +53,51 @@ export function TimelineAxis({ const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted; return ( - <Sticky disableCompensation> - {({ style }) => { - return ( - <div - style={{ - position: 'absolute', - borderBottom: `1px solid ${theme.eui.euiColorMediumShade}`, - height: px(margins.top), - zIndex: 2, - width: '100%', - ...style, - }} - > - <XYPlot - dontCheckIfEmpty - width={width} - height={margins.top} - margin={{ - top: margins.top, - left: margins.left, - right: margins.right, - }} - xDomain={xDomain} - > - <XAxis - hideLine - orientation="top" - tickSize={0} - tickValues={xAxisTickValues} - tickFormat={(time?: number) => tickFormatter(time).formatted} - tickPadding={20} - style={{ - text: { fill: theme.eui.euiColorDarkShade }, - }} - /> + <div + style={{ + position: 'sticky', + top: 0, + borderBottom: `1px solid ${theme.eui.euiColorMediumShade}`, + height: px(margins.top), + zIndex: 2, + width: '100%', + }} + > + <XYPlot + dontCheckIfEmpty + width={width} + height={margins.top} + margin={{ + top: margins.top, + left: margins.left, + right: margins.right, + }} + xDomain={xDomain} + > + <XAxis + hideLine + orientation="top" + tickSize={0} + tickValues={xAxisTickValues} + tickFormat={(time?: number) => tickFormatter(time).formatted} + tickPadding={20} + style={{ + text: { fill: theme.eui.euiColorDarkShade }, + }} + /> - {topTraceDuration > 0 && ( - <LastTickValue - x={xScale(topTraceDuration)} - value={topTraceDurationFormatted} - marginTop={28} - /> - )} + {topTraceDuration > 0 && ( + <LastTickValue + x={xScale(topTraceDuration)} + value={topTraceDurationFormatted} + marginTop={28} + /> + )} - {marks.map((mark) => ( - <Marker key={mark.id} mark={mark} x={xScale(mark.offset)} /> - ))} - </XYPlot> - </div> - ); - }} - </Sticky> + {marks.map((mark) => ( + <Marker key={mark.id} mark={mark} x={xScale(mark.offset)} /> + ))} + </XYPlot> + </div> ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap index 2756de6e384bc..76e2960e78e9d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap @@ -2,18 +2,11 @@ exports[`Timeline should render with data 1`] = ` <div - onScroll={[Function]} - onTouchEnd={[Function]} - onTouchMove={[Function]} - onTouchStart={[Function]} -> - <div - style={ - Object { - "height": "100%", - "width": "100%", - } + style={ + Object { + "height": "100%", + "width": "100%", } - /> -</div> + } +/> `; diff --git a/yarn.lock b/yarn.lock index af1a1493bc36a..73741371d10c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5537,13 +5537,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-sticky@^6.0.3": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/react-sticky/-/react-sticky-6.0.3.tgz#94d16a951467b29ad44c224081d9503e7e590434" - integrity sha512-tW0Y1hTr2Tao4yX58iKl0i7BaqrdObGXAzsyzd8VGVrWVEgbQuV6P6QKVd/kFC7FroXyelftiVNJ09pnfkcjww== - dependencies: - "@types/react" "*" - "@types/react-syntax-highlighter@11.0.4": version "11.0.4" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd" @@ -22892,7 +22885,7 @@ raf-schd@^4.0.0, raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@^3.1.0, raf@^3.3.0, raf@^3.4.1: +raf@^3.1.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -23570,14 +23563,6 @@ react-sizeme@^2.6.7: shallowequal "^1.1.0" throttle-debounce "^2.1.0" -react-sticky@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/react-sticky/-/react-sticky-6.0.3.tgz#7a18b643e1863da113d7f7036118d2a75d9ecde4" - integrity sha512-LNH4UJlRatOqo29/VHxDZOf6fwbgfgcHO4mkEFvrie5FuaZCSTGtug5R8NGqJ0kSnX8gHw8qZN37FcvnFBJpTQ== - dependencies: - prop-types "^15.5.8" - raf "^3.3.0" - react-style-singleton@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.0.tgz#7396885332e9729957f9df51f08cadbfc164e1c4" From 9cbf971427f6b2f4c3ca05b7d526c1ce790e8939 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger <scotty.bollinger@elastic.co> Date: Wed, 2 Dec 2020 21:19:52 -0600 Subject: [PATCH 081/107] [Enterprise Search] Fix schema errors button (#84842) * Fix schema errors button When migrated, the button was wrapping the link and it should be the other way around. This caused a blue link color. * Remove redundant true value * TIL EuiButtonTo --- .../indexing_status/indexing_status_errors.test.tsx | 9 ++++----- .../shared/indexing_status/indexing_status_errors.tsx | 11 ++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx index fc706aee659a5..563702a143ab3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCallOut, EuiButton } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import { IndexingStatusErrors } from './indexing_status_errors'; @@ -17,9 +17,8 @@ describe('IndexingStatusErrors', () => { it('renders', () => { const wrapper = shallow(<IndexingStatusErrors viewLinkPath="/path" />); - expect(wrapper.find(EuiButton)).toHaveLength(1); expect(wrapper.find(EuiCallOut)).toHaveLength(1); - expect(wrapper.find(EuiLinkTo)).toHaveLength(1); - expect(wrapper.find(EuiLinkTo).prop('to')).toEqual('/path'); + expect(wrapper.find(EuiButtonTo)).toHaveLength(1); + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/path'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx index a928400b2338c..2be27299fd77f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import { INDEXING_STATUS_HAS_ERRORS_TITLE, INDEXING_STATUS_HAS_ERRORS_BUTTON } from './constants'; @@ -24,8 +24,9 @@ export const IndexingStatusErrors: React.FC<IIndexingStatusErrorsProps> = ({ vie data-test-subj="IndexingStatusErrors" > <p>{INDEXING_STATUS_HAS_ERRORS_TITLE}</p> - <EuiButton color="danger" fill={true} size="s" data-test-subj="ViewErrorsButton"> - <EuiLinkTo to={viewLinkPath}>{INDEXING_STATUS_HAS_ERRORS_BUTTON}</EuiLinkTo> - </EuiButton> + + <EuiButtonTo to={viewLinkPath} color="danger" fill size="s" data-test-subj="ViewErrorsButton"> + {INDEXING_STATUS_HAS_ERRORS_BUTTON} + </EuiButtonTo> </EuiCallOut> ); From 78123a109d5eefe619ff71a46ebe952512b5daec Mon Sep 17 00:00:00 2001 From: Mikhail Shustov <restrry@gmail.com> Date: Thu, 3 Dec 2020 09:19:36 +0300 Subject: [PATCH 082/107] Rename server.xsrf.whitelist to server.xsrf.allowlist (#84791) * rename xsrd.whitelist to xsrf.allowlist * update docs * update telemetry schema * update kbn-config tests --- docs/api/using-api.asciidoc | 2 +- docs/apm/api.asciidoc | 2 +- docs/setup/settings.asciidoc | 4 ++-- .../legacy_object_to_config_adapter.test.ts.snap | 4 ++-- .../legacy_object_to_config_adapter.test.ts | 4 ++-- .../config/deprecation/core_deprecations.test.ts | 5 +++-- .../config/deprecation/core_deprecations.ts | 12 +----------- .../core_usage_data_service.mock.ts | 2 +- .../core_usage_data_service.test.ts | 2 +- .../core_usage_data/core_usage_data_service.ts | 2 +- src/core/server/core_usage_data/types.ts | 2 +- .../http/__snapshots__/http_config.test.ts.snap | 2 +- .../server/http/cookie_session_storage.test.ts | 2 +- src/core/server/http/http_config.test.ts | 6 +++--- src/core/server/http/http_config.ts | 4 ++-- .../integration_tests/lifecycle_handlers.test.ts | 8 ++++---- src/core/server/http/lifecycle_handlers.test.ts | 16 ++++++++-------- src/core/server/http/lifecycle_handlers.ts | 4 ++-- src/core/server/http/test_utils.ts | 2 +- src/core/server/server.api.md | 2 +- .../collectors/core/core_usage_collector.ts | 2 +- src/plugins/telemetry/schema/oss_plugins.json | 2 +- .../alerting_api_integration/common/config.ts | 2 +- 23 files changed, 42 insertions(+), 51 deletions(-) diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index c796aac3d6b27..d66718be4074a 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -61,7 +61,7 @@ For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsr By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: * The API endpoint uses the `GET` or `HEAD` operations -* The path is whitelisted using the <<settings-xsrf-whitelist, `server.xsrf.whitelist`>> setting +* The path is allowed using the <<settings-xsrf-allowlist, `server.xsrf.allowlist`>> setting * XSRF protections are disabled using the <<settings-xsrf-disableProtection, `server.xsrf.disableProtection`>> setting `Content-Type: application/json`:: diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 01ba084b9e9e7..d9a8d0558714f 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -40,7 +40,7 @@ users interacting with APM APIs must have <<apm-app-api-user,sufficient privileg By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: * The API endpoint uses the `GET` or `HEAD` operations -* The path is whitelisted using the <<settings-xsrf-whitelist, `server.xsrf.whitelist`>> setting +* The path is allowed using the <<settings-xsrf-allowlist, `server.xsrf.allowlist`>> setting * XSRF protections are disabled using the <<settings-xsrf-disableProtection, `server.xsrf.disableProtection`>> setting `Content-Type: application/json`:: diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c22d4466ee09e..3786cbc7d83b6 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -575,10 +575,10 @@ all http requests to https over the port configured as <<server-port, `server.po | An array of supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`, `TLSv1.3`. *Default: TLSv1.1, TLSv1.2, TLSv1.3* -| [[settings-xsrf-whitelist]] `server.xsrf.whitelist:` +| [[settings-xsrf-allowlist]] `server.xsrf.allowlist:` | It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the `kbn-xsrf` header. -The <<settings-xsrf-whitelist, `server.xsrf.whitelist`>> setting requires the following format: +The <<settings-xsrf-allowlist, `server.xsrf.allowlist`>> setting requires the following format: |=== diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 4a6d86a0dfba6..5d8fb1e28beb6 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -25,8 +25,8 @@ Object { }, "uuid": undefined, "xsrf": Object { + "allowlist": Array [], "disableProtection": false, - "whitelist": Array [], }, } `; @@ -56,8 +56,8 @@ Object { }, "uuid": undefined, "xsrf": Object { + "allowlist": Array [], "disableProtection": false, - "whitelist": Array [], }, } `; diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 1c51564187442..036ff5e80b3ec 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -96,7 +96,7 @@ describe('#get', () => { someNotSupportedValue: 'val', xsrf: { disableProtection: false, - whitelist: [], + allowlist: [], }, }, }); @@ -119,7 +119,7 @@ describe('#get', () => { someNotSupportedValue: 'val', xsrf: { disableProtection: false, - whitelist: [], + allowlist: [], }, }, }); diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 7a69dc2fa726e..c645629fa5653 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -82,12 +82,13 @@ describe('core deprecations', () => { describe('xsrfDeprecation', () => { it('logs a warning if server.xsrf.whitelist is set', () => { - const { messages } = applyCoreDeprecations({ + const { migrated, messages } = applyCoreDeprecations({ server: { xsrf: { whitelist: ['/path'] } }, }); + expect(migrated.server.xsrf.allowlist).toEqual(['/path']); expect(messages).toMatchInlineSnapshot(` Array [ - "It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. It will be removed in 8.0 release. Instead, supply the \\"kbn-xsrf\\" header.", + "\\"server.xsrf.whitelist\\" is deprecated and has been replaced by \\"server.xsrf.allowlist\\"", ] `); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 6c85cfbed8e82..3dde7cfb6c1cb 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -38,16 +38,6 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; -const xsrfDeprecation: ConfigDeprecation = (settings, fromPath, log) => { - if ((settings.server?.xsrf?.whitelist ?? []).length > 0) { - log( - 'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' + - 'It will be removed in 8.0 release. Instead, supply the "kbn-xsrf" header.' - ); - } - return settings; -}; - const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { log( @@ -140,10 +130,10 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu unusedFromRoot('elasticsearch.startupTimeout'), rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'), rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'), + rename('server.xsrf.whitelist', 'server.xsrf.allowlist'), configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, cspRulesDeprecation, mapManifestServiceUrlDeprecation, - xsrfDeprecation, ]; diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 523256129333f..b1c731e8ba534 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -99,7 +99,7 @@ const createStartContractMock = () => { }, xsrf: { disableProtection: false, - whitelistConfigured: false, + allowlistConfigured: false, }, }, logging: { diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index e1c78edb902a9..6686a778ee8a5 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -182,8 +182,8 @@ describe('CoreUsageDataService', () => { "truststoreConfigured": false, }, "xsrf": Object { + "allowlistConfigured": false, "disableProtection": false, - "whitelistConfigured": false, }, }, "logging": Object { diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index f729e23cb68bc..490c411ecb852 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -180,7 +180,7 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar }, xsrf: { disableProtection: http.xsrf.disableProtection, - whitelistConfigured: isConfigured.array(http.xsrf.whitelist), + allowlistConfigured: isConfigured.array(http.xsrf.allowlist), }, requestId: { allowFromAnyIp: http.requestId.allowFromAnyIp, diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 52d2eadcf1377..258f452cfa6ae 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -100,7 +100,7 @@ export interface CoreConfigUsageData { }; xsrf: { disableProtection: boolean; - whitelistConfigured: boolean; + allowlistConfigured: boolean; }; requestId: { allowFromAnyIp: boolean; diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 8e8891b8a73aa..daea60122c3cb 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -83,8 +83,8 @@ Object { "truststore": Object {}, }, "xsrf": Object { + "allowlist": Array [], "disableProtection": false, - "whitelist": Array [], }, } `; diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index b7ade0cbde0fc..7ac7e4b9712d0 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -60,7 +60,7 @@ configService.atPath.mockReturnValue( compression: { enabled: true }, xsrf: { disableProtection: true, - whitelist: [], + allowlist: [], }, customResponseHeaders: {}, requestId: { diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 58e6699582e13..c843773da72bb 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -165,15 +165,15 @@ test('uses os.hostname() as default for server.name', () => { expect(validated.name).toEqual('kibana-hostname'); }); -test('throws if xsrf.whitelist element does not start with a slash', () => { +test('throws if xsrf.allowlist element does not start with a slash', () => { const httpSchema = config.schema; const obj = { xsrf: { - whitelist: ['/valid-path', 'invalid-path'], + allowlist: ['/valid-path', 'invalid-path'], }, }; expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"[xsrf.whitelist.1]: must start with a slash"` + `"[xsrf.allowlist.1]: must start with a slash"` ); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 7d41b4ea9e915..be64def294625 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -82,7 +82,7 @@ export const config = { ), xsrf: schema.object({ disableProtection: schema.boolean({ defaultValue: false }), - whitelist: schema.arrayOf( + allowlist: schema.arrayOf( schema.string({ validate: match(/^\//, 'must start with a slash') }), { defaultValue: [] } ), @@ -142,7 +142,7 @@ export class HttpConfig { public ssl: SslConfig; public compression: { enabled: boolean; referrerWhitelist?: string[] }; public csp: ICspConfig; - public xsrf: { disableProtection: boolean; whitelist: string[] }; + public xsrf: { disableProtection: boolean; allowlist: string[] }; public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; /** diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index a964130550bf5..7df35b04c66cf 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -36,7 +36,7 @@ const actualVersion = pkg.version; const versionHeader = 'kbn-version'; const xsrfHeader = 'kbn-xsrf'; const nameHeader = 'kbn-name'; -const whitelistedTestPath = '/xsrf/test/route/whitelisted'; +const allowlistedTestPath = '/xsrf/test/route/whitelisted'; const xsrfDisabledTestPath = '/xsrf/test/route/disabled'; const kibanaName = 'my-kibana-name'; const setupDeps = { @@ -63,7 +63,7 @@ describe('core lifecycle handlers', () => { customResponseHeaders: { 'some-header': 'some-value', }, - xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] }, + xsrf: { disableProtection: false, allowlist: [allowlistedTestPath] }, requestId: { allowFromAnyIp: true, ipAllowlist: [], @@ -179,7 +179,7 @@ describe('core lifecycle handlers', () => { } ); ((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>( - { path: whitelistedTestPath, validate: false }, + { path: allowlistedTestPath, validate: false }, (context, req, res) => { return res.ok({ body: 'ok' }); } @@ -235,7 +235,7 @@ describe('core lifecycle handlers', () => { }); it('accepts whitelisted requests without either an xsrf or version header', async () => { - await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok'); + await getSupertest(method.toLowerCase(), allowlistedTestPath).expect(200, 'ok'); }); it('accepts requests on a route with disabled xsrf protection', async () => { diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index fdcf2a173b906..8ad823b3a6944 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -58,7 +58,7 @@ describe('xsrf post-auth handler', () => { describe('non destructive methods', () => { it('accepts requests without version or xsrf header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'get', headers: {} }); @@ -74,7 +74,7 @@ describe('xsrf post-auth handler', () => { describe('destructive methods', () => { it('accepts requests with xsrf header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: { 'kbn-xsrf': 'xsrf' } }); @@ -88,7 +88,7 @@ describe('xsrf post-auth handler', () => { }); it('accepts requests with version header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: { 'kbn-version': 'some-version' } }); @@ -102,7 +102,7 @@ describe('xsrf post-auth handler', () => { }); it('returns a bad request if called without xsrf or version header', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post' }); @@ -121,7 +121,7 @@ describe('xsrf post-auth handler', () => { }); it('accepts requests if protection is disabled', () => { - const config = createConfig({ xsrf: { whitelist: [], disableProtection: true } }); + const config = createConfig({ xsrf: { allowlist: [], disableProtection: true } }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: {} }); @@ -134,9 +134,9 @@ describe('xsrf post-auth handler', () => { expect(result).toEqual('next'); }); - it('accepts requests if path is whitelisted', () => { + it('accepts requests if path is allowlisted', () => { const config = createConfig({ - xsrf: { whitelist: ['/some-path'], disableProtection: false }, + xsrf: { allowlist: ['/some-path'], disableProtection: false }, }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ method: 'post', headers: {}, path: '/some-path' }); @@ -152,7 +152,7 @@ describe('xsrf post-auth handler', () => { it('accepts requests if xsrf protection on a route is disabled', () => { const config = createConfig({ - xsrf: { whitelist: [], disableProtection: false }, + xsrf: { allowlist: [], disableProtection: false }, }); const handler = createXsrfPostAuthHandler(config); const request = forgeRequest({ diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts index 7ef7e86326039..4060284b5b56a 100644 --- a/src/core/server/http/lifecycle_handlers.ts +++ b/src/core/server/http/lifecycle_handlers.ts @@ -29,12 +29,12 @@ const XSRF_HEADER = 'kbn-xsrf'; const KIBANA_NAME_HEADER = 'kbn-name'; export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler => { - const { whitelist, disableProtection } = config.xsrf; + const { allowlist, disableProtection } = config.xsrf; return (request, response, toolkit) => { if ( disableProtection || - whitelist.includes(request.route.path) || + allowlist.includes(request.route.path) || request.route.options.xsrfRequired === false ) { return toolkit.next(); diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index 412396644648e..cdcbe513e1224 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -43,7 +43,7 @@ configService.atPath.mockReturnValue( compression: { enabled: true }, xsrf: { disableProtection: true, - whitelist: [], + allowlist: [], }, customResponseHeaders: {}, requestId: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 36a8d9a52fd52..59f9c4f9ff38c 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -409,7 +409,7 @@ export interface CoreConfigUsageData { }; xsrf: { disableProtection: boolean; - whitelistConfigured: boolean; + allowlistConfigured: boolean; }; requestId: { allowFromAnyIp: boolean; diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index 3fd011b0bded2..a514f9f899e55 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -65,7 +65,7 @@ export function getCoreUsageCollector( }, xsrf: { disableProtection: { type: 'boolean' }, - whitelistConfigured: { type: 'boolean' }, + allowlistConfigured: { type: 'boolean' }, }, requestId: { allowFromAnyIp: { type: 'boolean' }, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 3d79d7c6cf0e1..e1078c60caf2e 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1391,7 +1391,7 @@ "disableProtection": { "type": "boolean" }, - "whitelistConfigured": { + "allowlistConfigured": { "type": "boolean" } } diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index cb78e76bdd697..866dd0581b548 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -143,7 +143,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` ), - `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + `--server.xsrf.allowlist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, From 155d150a089b99f35506567aea36b41d70cccd19 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm <matthias.wilhelm@elastic.co> Date: Thu, 3 Dec 2020 09:23:14 +0100 Subject: [PATCH 083/107] [Discover] Unskip date histogram test (#84727) --- test/functional/apps/discover/_discover_histogram.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 672becca614c9..e06783174e83b 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'dateFormat:tz': 'Europe/Berlin', }; - // FLAKY: https://github.com/elastic/kibana/issues/81576 - describe.skip('discover histogram', function describeIndexTests() { + describe('discover histogram', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); From 770a00530d8afd8bf534d4e559137844821e3334 Mon Sep 17 00:00:00 2001 From: Thomas Watson <w@tson.dk> Date: Thu, 3 Dec 2020 09:33:48 +0100 Subject: [PATCH 084/107] Catch @hapi/podium errors (#84575) --- .../src/legacy_logging_server.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index 1b13eda44fff2..1533bde4fc17b 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -117,11 +117,18 @@ export class LegacyLoggingServer { public log({ level, context, message, error, timestamp, meta = {} }: LogRecord) { const { tags = [], ...metadata } = meta; - this.events.emit('log', { - data: getDataToLog(error, metadata, message), - tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], - timestamp: timestamp.getTime(), - }); + this.events + .emit('log', { + data: getDataToLog(error, metadata, message), + tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], + timestamp: timestamp.getTime(), + }) + // @ts-expect-error @hapi/podium emit is actually an async function + .catch((err) => { + // eslint-disable-next-line no-console + console.error('An unexpected error occurred while writing to the log:', err.stack); + process.exit(1); + }); } public stop() { From 3ae73653ff8ca59f2731415f506d414f486db031 Mon Sep 17 00:00:00 2001 From: Thomas Watson <w@tson.dk> Date: Thu, 3 Dec 2020 09:34:45 +0100 Subject: [PATCH 085/107] Improve logging pipeline in @kbn/legacy-logging (#84629) --- .../src/log_reporter.test.ts | 142 ++++++++++++++++++ .../kbn-legacy-logging/src/log_reporter.ts | 29 ++-- 2 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 packages/kbn-legacy-logging/src/log_reporter.test.ts diff --git a/packages/kbn-legacy-logging/src/log_reporter.test.ts b/packages/kbn-legacy-logging/src/log_reporter.test.ts new file mode 100644 index 0000000000000..4fa2922c7824e --- /dev/null +++ b/packages/kbn-legacy-logging/src/log_reporter.test.ts @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import os from 'os'; +import path from 'path'; +import fs from 'fs'; + +import stripAnsi from 'strip-ansi'; + +import { getLogReporter } from './log_reporter'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('getLogReporter', () => { + it('should log to stdout (not json)', async () => { + const lines: string[] = []; + const origWrite = process.stdout.write; + process.stdout.write = (buffer: string | Uint8Array): boolean => { + lines.push(stripAnsi(buffer.toString()).trim()); + return true; + }; + + const loggerStream = getLogReporter({ + config: { + json: false, + dest: 'stdout', + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + process.stdout.write = origWrite; + expect(lines.length).toBe(1); + expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/); + }); + + it('should log to stdout (as json)', async () => { + const lines: string[] = []; + const origWrite = process.stdout.write; + process.stdout.write = (buffer: string | Uint8Array): boolean => { + lines.push(JSON.parse(buffer.toString().trim())); + return true; + }; + + const loggerStream = getLogReporter({ + config: { + json: true, + dest: 'stdout', + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + process.stdout.write = origWrite; + expect(lines.length).toBe(1); + expect(lines[0]).toMatchObject({ + type: 'log', + tags: ['foo'], + message: 'hello world', + }); + }); + + it('should log to custom file (not json)', async () => { + const dir = os.tmpdir(); + const logfile = `dest-${Date.now()}.log`; + const dest = path.join(dir, logfile); + + const loggerStream = getLogReporter({ + config: { + json: false, + dest, + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + const lines = stripAnsi(fs.readFileSync(dest, { encoding: 'utf8' })) + .trim() + .split(os.EOL); + expect(lines.length).toBe(1); + expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/); + }); + + it('should log to custom file (as json)', async () => { + const dir = os.tmpdir(); + const logfile = `dest-${Date.now()}.log`; + const dest = path.join(dir, logfile); + + const loggerStream = getLogReporter({ + config: { + json: true, + dest, + filter: {}, + }, + events: { log: '*' }, + }); + + loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' }); + + await sleep(500); + + const lines = fs + .readFileSync(dest, { encoding: 'utf8' }) + .trim() + .split(os.EOL) + .map((data) => JSON.parse(data)); + expect(lines.length).toBe(1); + expect(lines[0]).toMatchObject({ + type: 'log', + tags: ['foo'], + message: 'hello world', + }); + }); +}); diff --git a/packages/kbn-legacy-logging/src/log_reporter.ts b/packages/kbn-legacy-logging/src/log_reporter.ts index 8ecaf348bac04..f0075b431b83d 100644 --- a/packages/kbn-legacy-logging/src/log_reporter.ts +++ b/packages/kbn-legacy-logging/src/log_reporter.ts @@ -17,9 +17,11 @@ * under the License. */ +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream'; + // @ts-expect-error missing type def import { Squeeze } from '@hapi/good-squeeze'; -import { createWriteStream as writeStr, WriteStream } from 'fs'; import { KbnLoggerJsonFormat } from './log_format_json'; import { KbnLoggerStringFormat } from './log_format_string'; @@ -31,21 +33,28 @@ export function getLogReporter({ events, config }: { events: any; config: LogFor const format = config.json ? new KbnLoggerJsonFormat(config) : new KbnLoggerStringFormat(config); const logInterceptor = new LogInterceptor(); - let dest: WriteStream | NodeJS.WritableStream; if (config.dest === 'stdout') { - dest = process.stdout; + pipeline(logInterceptor, squeeze, format, onFinished); + // The `pipeline` function is used to properly close all streams in the + // pipeline in case one of them ends or fails. Since stdout obviously + // shouldn't be closed in case of a failure in one of the other streams, + // we're not including that in the call to `pipeline`, but rely on the old + // `pipe` function instead. + format.pipe(process.stdout); } else { - dest = writeStr(config.dest, { + const dest = createWriteStream(config.dest, { flags: 'a', encoding: 'utf8', }); - - logInterceptor.on('end', () => { - dest.end(); - }); + pipeline(logInterceptor, squeeze, format, dest, onFinished); } - logInterceptor.pipe(squeeze).pipe(format).pipe(dest); - return logInterceptor; } + +function onFinished(err: NodeJS.ErrnoException | null) { + if (err) { + // eslint-disable-next-line no-console + console.error('An unexpected error occurred in the logging pipeline:', err.stack); + } +} From 9041ea5478b2e7b31f2de1270f00d2a11530f4ac Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Thu, 3 Dec 2020 09:41:45 +0100 Subject: [PATCH 086/107] [Lens] Migrate legacy es client and remove total hits as int (#84340) --- .../lens/server/routes/existing_fields.ts | 11 +++--- .../plugins/lens/server/routes/field_stats.ts | 35 +++++++++---------- .../plugins/lens/server/routes/telemetry.ts | 3 +- x-pack/plugins/lens/server/usage/task.ts | 28 +++++++-------- .../lens/server/usage/visualization_counts.ts | 8 ++--- .../api_integration/apis/lens/telemetry.ts | 28 +++++++-------- 6 files changed, 54 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index aef8b1b3d7076..43c56af7f71bc 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -5,8 +5,9 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext, ElasticsearchClient } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; import { IndexPattern, IndexPatternsService } from 'src/plugins/data/common'; import { BASE_API_URL } from '../../common'; @@ -68,7 +69,7 @@ export async function existingFieldsRoute(setup: CoreSetup<PluginStartContract>, logger.info( `Field existence check failed: ${isBoomError(e) ? e.output.payload.message : e.message}` ); - if (e.status === 404) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound({ body: e.message }); } if (isBoomError(e)) { @@ -111,7 +112,7 @@ async function fetchFieldExistence({ fromDate, toDate, dslQuery, - client: context.core.elasticsearch.legacy.client, + client: context.core.elasticsearch.client.asCurrentUser, index: indexPattern.title, timeFieldName: timeFieldName || indexPattern.timeFieldName, fields, @@ -149,7 +150,7 @@ async function fetchIndexPatternStats({ toDate, fields, }: { - client: ILegacyScopedClusterClient; + client: ElasticsearchClient; index: string; dslQuery: object; timeFieldName?: string; @@ -179,7 +180,7 @@ async function fetchIndexPatternStats({ }; const scriptedFields = fields.filter((f) => f.isScript); - const result = await client.callAsCurrentUser('search', { + const { body: result } = await client.search({ index, body: { size: SAMPLE_SIZE, diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index e0f1e05ed970d..21dfb90ec0ff4 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -5,6 +5,7 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; @@ -47,7 +48,7 @@ export async function initFieldsRoute(setup: CoreSetup<PluginStartContract>) { }, }, async (context, req, res) => { - const requestClient = context.core.elasticsearch.legacy.client; + const requestClient = context.core.elasticsearch.client.asCurrentUser; const { fromDate, toDate, timeFieldName, field, dslQuery } = req.body; try { @@ -71,18 +72,18 @@ export async function initFieldsRoute(setup: CoreSetup<PluginStartContract>) { }, }; - const search = (aggs: unknown) => - requestClient.callAsCurrentUser('search', { + const search = async (aggs: unknown) => { + const { body: result } = await requestClient.search({ index: req.params.indexPatternTitle, + track_total_hits: true, body: { query, aggs, }, - // The hits total changed in 7.0 from number to object, unless this flag is set - // this is a workaround for elasticsearch response types that are from 6.x - restTotalHitsAsInt: true, size: 0, }); + return result; + }; if (field.type === 'number') { return res.ok({ @@ -98,7 +99,7 @@ export async function initFieldsRoute(setup: CoreSetup<PluginStartContract>) { body: await getStringSamples(search, field), }); } catch (e) { - if (e.status === 404) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound(); } if (e.isBoom) { @@ -142,8 +143,7 @@ export async function getNumberHistogram( const minMaxResult = (await aggSearchWithBody(searchBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof searchBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof searchBody } } >; const minValue = minMaxResult.aggregations!.sample.min_value.value; @@ -164,7 +164,7 @@ export async function getNumberHistogram( if (histogramInterval === 0) { return { - totalDocuments: minMaxResult.hits.total, + totalDocuments: minMaxResult.hits.total.value, sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, sampledDocuments: minMaxResult.aggregations!.sample.doc_count, topValues: topValuesBuckets, @@ -187,12 +187,11 @@ export async function getNumberHistogram( }; const histogramResult = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof histogramBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof histogramBody } } >; return { - totalDocuments: minMaxResult.hits.total, + totalDocuments: minMaxResult.hits.total.value, sampledDocuments: minMaxResult.aggregations!.sample.doc_count, sampledValues: minMaxResult.aggregations!.sample.sample_count.value!, histogram: { @@ -227,12 +226,11 @@ export async function getStringSamples( }; const topValuesResult = (await aggSearchWithBody(topValuesBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof topValuesBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof topValuesBody } } >; return { - totalDocuments: topValuesResult.hits.total, + totalDocuments: topValuesResult.hits.total.value, sampledDocuments: topValuesResult.aggregations!.sample.doc_count, sampledValues: topValuesResult.aggregations!.sample.sample_count.value!, topValues: { @@ -275,12 +273,11 @@ export async function getDateHistogram( }; const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse< unknown, - { body: { aggs: typeof histogramBody } }, - { restTotalHitsAsInt: true } + { body: { aggs: typeof histogramBody } } >; return { - totalDocuments: results.hits.total, + totalDocuments: results.hits.total.value, histogram: { buckets: results.aggregations!.histo.buckets.map((bucket) => ({ count: bucket.doc_count, diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index 820e32509923e..2bd891e7c1376 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -5,6 +5,7 @@ */ import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { BASE_API_URL } from '../../common'; @@ -71,7 +72,7 @@ export async function initLensUsageRoute(setup: CoreSetup<PluginStartContract>) return res.ok({ body: {} }); } catch (e) { - if (e.status === 404) { + if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound(); } if (e.isBoom) { diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 014193fb6566e..0fd797bba68e4 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, CoreSetup, Logger } from 'kibana/server'; +import { CoreSetup, Logger, ElasticsearchClient } from 'kibana/server'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import moment from 'moment'; @@ -69,11 +69,12 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export async function getDailyEvents( kibanaIndex: string, - callCluster: LegacyAPICaller + getEsClient: () => Promise<ElasticsearchClient> ): Promise<{ byDate: Record<string, Record<string, number>>; suggestionsByDate: Record<string, Record<string, number>>; }> { + const esClient = await getEsClient(); const aggs = { daily: { date_histogram: { @@ -114,15 +115,10 @@ export async function getDailyEvents( }, }; - const metrics: ESSearchResponse< - unknown, - { - body: { aggs: typeof aggs }; - }, - { restTotalHitsAsInt: true } - > = await callCluster('search', { + const { body: metrics } = await esClient.search< + ESSearchResponse<unknown, { body: { aggs: typeof aggs } }> + >({ index: kibanaIndex, - rest_total_hits_as_int: true, body: { query: { bool: { @@ -156,9 +152,9 @@ export async function getDailyEvents( }); // Always delete old date because we don't report it - await callCluster('deleteByQuery', { + await esClient.deleteByQuery({ index: kibanaIndex, - waitForCompletion: true, + wait_for_completion: true, body: { query: { bool: { @@ -184,9 +180,9 @@ export function telemetryTaskRunner( ) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = async (...args: Parameters<LegacyAPICaller>) => { + const getEsClient = async () => { const [coreStart] = await core.getStartServices(); - return coreStart.elasticsearch.legacy.client.callAsInternalUser(...args); + return coreStart.elasticsearch.client.asInternalUser; }; return { @@ -194,8 +190,8 @@ export function telemetryTaskRunner( const kibanaIndex = (await config.pipe(first()).toPromise()).kibana.index; return Promise.all([ - getDailyEvents(kibanaIndex, callCluster), - getVisualizationCounts(callCluster, kibanaIndex), + getDailyEvents(kibanaIndex, getEsClient), + getVisualizationCounts(getEsClient, kibanaIndex), ]) .then(([lensTelemetry, lensVisualizations]) => { return { diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts index c9cd4aff72b2b..f6858ef941b78 100644 --- a/x-pack/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { LensVisualizationUsage } from './types'; export async function getVisualizationCounts( - callCluster: LegacyAPICaller, + getEsClient: () => Promise<ElasticsearchClient>, kibanaIndex: string ): Promise<LensVisualizationUsage> { - const results = await callCluster('search', { + const esClient = await getEsClient(); + const { body: results } = await esClient.search({ index: kibanaIndex, - rest_total_hits_as_int: true, body: { query: { bool: { diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 5525a82b02ee8..d352d250aee69 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -6,8 +6,7 @@ import moment from 'moment'; import expect from '@kbn/expect'; -import { Client, SearchParams } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { Client } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -20,10 +19,7 @@ const COMMON_HEADERS = { export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es: Client = getService('legacyEs'); - const callCluster: LegacyAPICaller = (((path: 'search', searchParams: SearchParams) => { - return es[path].call(es, searchParams); - }) as unknown) as LegacyAPICaller; + const es: Client = getService('es'); async function assertExpectedSavedObjects(num: number) { // Make sure that new/deleted docs are available to search @@ -31,7 +27,9 @@ export default ({ getService }: FtrProviderContext) => { index: '.kibana', }); - const { count } = await es.count({ + const { + body: { count }, + } = await es.count({ index: '.kibana', q: 'type:lens-ui-telemetry', }); @@ -44,8 +42,9 @@ export default ({ getService }: FtrProviderContext) => { await es.deleteByQuery({ index: '.kibana', q: 'type:lens-ui-telemetry', - waitForCompletion: true, - refresh: 'wait_for', + wait_for_completion: true, + refresh: true, + body: {}, }); }); @@ -53,8 +52,9 @@ export default ({ getService }: FtrProviderContext) => { await es.deleteByQuery({ index: '.kibana', q: 'type:lens-ui-telemetry', - waitForCompletion: true, - refresh: 'wait_for', + wait_for_completion: true, + refresh: true, + body: {}, }); }); @@ -107,7 +107,7 @@ export default ({ getService }: FtrProviderContext) => { refresh: 'wait_for', }); - const result = await getDailyEvents('.kibana', callCluster); + const result = await getDailyEvents('.kibana', () => Promise.resolve(es)); expect(result).to.eql({ byDate: {}, @@ -150,7 +150,7 @@ export default ({ getService }: FtrProviderContext) => { ], }); - const result = await getDailyEvents('.kibana', callCluster); + const result = await getDailyEvents('.kibana', () => Promise.resolve(es)); expect(result).to.eql({ byDate: { @@ -177,7 +177,7 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.loadIfNeeded('lens/basic'); - const results = await getVisualizationCounts(callCluster, '.kibana'); + const results = await getVisualizationCounts(() => Promise.resolve(es), '.kibana'); expect(results).to.have.keys([ 'saved_overall', From f1495eda1f063b6910837d1fc2d40ad1cc2577c7 Mon Sep 17 00:00:00 2001 From: Alexey Antonov <alexwizp@gmail.com> Date: Thu, 3 Dec 2020 12:34:31 +0300 Subject: [PATCH 087/107] [TSVB] [Cleanup] Remove extra dateFormat props (#84749) --- .../public/application/components/markdown_editor.js | 6 +++--- .../public/application/components/panel_config.js | 2 +- .../application/components/panel_config/markdown.js | 1 - .../components/timeseries_visualization.tsx | 1 - .../public/application/components/vis_editor.js | 1 - .../public/application/components/vis_types/index.ts | 1 - .../application/components/vis_types/markdown/vis.js | 5 ++--- .../components/vis_types/timeseries/vis.js | 12 +++++------- 8 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js index 904a27dcb23c2..4b5038b82f480 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js @@ -49,12 +49,12 @@ export class MarkdownEditor extends Component { } render() { - const { visData, model, dateFormat } = this.props; + const { visData, model, getConfig } = this.props; if (!visData) { return null; } - + const dateFormat = getConfig('dateFormat'); const series = _.get(visData, `${model.id}.series`, []); const variables = convertSeriesToVars(series, model, dateFormat, this.props.getConfig); const rows = []; @@ -214,6 +214,6 @@ export class MarkdownEditor extends Component { MarkdownEditor.propTypes = { onChange: PropTypes.func, model: PropTypes.object, - dateFormat: PropTypes.string, + getConfig: PropTypes.func, visData: PropTypes.object, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js index 3b081d8eb7db9..999127a9eb556 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js @@ -90,6 +90,6 @@ PanelConfig.propTypes = { fields: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - dateFormat: PropTypes.string, visData$: PropTypes.object, + getConfig: PropTypes.func, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index 36d0e3a80e227..ef7aec61a2f0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -334,7 +334,6 @@ MarkdownPanelConfigUi.propTypes = { fields: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - dateFormat: PropTypes.string, }; export const MarkdownPanelConfig = injectI18n(MarkdownPanelConfigUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index 5b5c99b970854..454f6ff855b38 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -96,7 +96,6 @@ function TimeseriesVisualization({ if (VisComponent) { return ( <VisComponent - dateFormat={getConfig('dateFormat')} getConfig={getConfig} model={model} visData={visData} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 2cc50bd7efaeb..520ad281576cd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -181,7 +181,6 @@ export class VisEditor extends Component { fields={this.state.visFields} model={model} visData$={this.visData$} - dateFormat={this.props.config.get('dateFormat')} onChange={this.handleChange} getConfig={this.getConfig} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 56e58b4da3458..e6104ad08fe9e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -64,6 +64,5 @@ export interface TimeseriesVisProps { ) => void; uiState: PersistedState; visData: TimeseriesVisData; - dateFormat: string; getConfig: IUiSettingsClient['get']; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js index e68b9e5ed8467..ffc6bf0dda2d4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js @@ -31,9 +31,9 @@ import { isBackgroundInverted } from '../../../lib/set_is_reversed'; const getMarkdownId = (id) => `markdown-${id}`; function MarkdownVisualization(props) { - const { backgroundColor, model, visData, dateFormat } = props; + const { backgroundColor, model, visData, getConfig } = props; const series = get(visData, `${model.id}.series`, []); - const variables = convertSeriesToVars(series, model, dateFormat, props.getConfig); + const variables = convertSeriesToVars(series, model, getConfig('dateFormat'), props.getConfig); const markdownElementId = getMarkdownId(uuid.v1()); const panelBackgroundColor = model.background_color || backgroundColor; @@ -103,7 +103,6 @@ MarkdownVisualization.propTypes = { onBrush: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, - dateFormat: PropTypes.string, getConfig: PropTypes.func, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index b752699fa1548..41837fbfb1d21 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -39,20 +39,18 @@ class TimeseriesVisualization extends Component { model: PropTypes.object, onBrush: PropTypes.func, visData: PropTypes.object, - dateFormat: PropTypes.string, getConfig: PropTypes.func, }; - xAxisFormatter = (interval) => (val) => { - const scaledDataFormat = this.props.getConfig('dateFormat:scaled'); - const { dateFormat } = this.props; + scaledDataFormat = this.props.getConfig('dateFormat:scaled'); + dateFormat = this.props.getConfig('dateFormat'); - if (!scaledDataFormat || !dateFormat) { + xAxisFormatter = (interval) => (val) => { + if (!this.scaledDataFormat || !this.dateFormat) { return val; } - const formatter = createXaxisFormatter(interval, scaledDataFormat, dateFormat); - + const formatter = createXaxisFormatter(interval, this.scaledDataFormat, this.dateFormat); return formatter(val); }; From 17d986e499f9877c28063fce1e941e25e04b7024 Mon Sep 17 00:00:00 2001 From: Marco Liberati <dej611@users.noreply.github.com> Date: Thu, 3 Dec 2020 10:46:51 +0100 Subject: [PATCH 088/107] [Embeddable] Export CSV action for Lens embeddables in dashboard (#83654) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ublic.uiactionsservice.addtriggeraction.md | 2 +- ...tions-public.uiactionsservice.getaction.md | 2 +- ...blic.uiactionsservice.gettriggeractions.md | 2 +- ...ionsservice.gettriggercompatibleactions.md | 2 +- ...gins-ui_actions-public.uiactionsservice.md | 10 +- ...-public.uiactionsservice.registeraction.md | 2 +- .../download-underlying-data.asciidoc | 15 ++ .../dashboard/explore-dashboard-data.asciidoc | 1 + .../images/download_csv_context_menu.png | Bin 0 -> 153535 bytes src/plugins/dashboard/kibana.json | 5 +- .../actions/export_csv_action.test.tsx | 134 +++++++++++++++++ .../application/actions/export_csv_action.tsx | 138 ++++++++++++++++++ .../public/application/actions/index.ts | 1 + src/plugins/dashboard/public/plugin.tsx | 13 +- .../data/common/exports/export_csv.tsx | 2 +- .../contact_card_exportable_embeddable.tsx | 44 ++++++ ...act_card_exportable_embeddable_factory.tsx | 85 +++++++++++ .../embeddables/contact_card/index.ts | 2 + src/plugins/ui_actions/public/public.api.md | 10 +- .../embeddable/embeddable.tsx | 14 ++ .../embeddable/expression_wrapper.tsx | 4 + x-pack/plugins/lens/public/plugin.ts | 18 ++- x-pack/test/functional/apps/lens/dashboard.ts | 15 ++ 23 files changed, 495 insertions(+), 26 deletions(-) create mode 100644 docs/user/dashboard/download-underlying-data.asciidoc create mode 100644 docs/user/dashboard/images/download_csv_context_menu.png create mode 100644 src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx create mode 100644 src/plugins/dashboard/public/application/actions/export_csv_action.tsx create mode 100644 src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx create mode 100644 src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index 5a1ab83551d34..fd6ade88479af 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -11,5 +11,5 @@ <b>Signature:</b> ```typescript -readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">) => void; +readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md index 5b0b3eea01cb1..d540de7637441 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md @@ -7,5 +7,5 @@ <b>Signature:</b> ```typescript -readonly getAction: <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; +readonly getAction: <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index 2dda422046318..0a9b674a45de2 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ <b>Signature:</b> ```typescript -readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]; +readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index e087753726a8a..faed81236342d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ <b>Signature:</b> ```typescript -readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]>; +readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index f9eb693b492f7..e3c5dbb92ae90 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,19 +21,19 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | <code>ActionRegistry</code> | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">) => void</code> | <code>addTriggerAction</code> is similar to <code>attachAction</code> as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.<code>addTriggerAction</code> also infers better typing of the <code>action</code> argument. | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void</code> | <code>addTriggerAction</code> is similar to <code>attachAction</code> as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.<code>addTriggerAction</code> also infers better typing of the <code>action</code> argument. | | [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void</code> | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | <code>() => void</code> | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | <code>(triggerId: TriggerId, actionId: string) => void</code> | | | [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void></code> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | <code>UiActionsExecutionService</code> | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | <code>() => UiActionsService</code> | "Fork" a separate instance of <code>UiActionsService</code> that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of <code>UiActionsService</code> are only available within this instance. | -| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <code><T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"></code> | | +| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <code><T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"></code> | | | [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T></code> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]</code> | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]></code> | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]</code> | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <code><T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]></code> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | <code>(actionId: string) => boolean</code> | | -| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <code><A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION"></code> | | +| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <code><A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"></code> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | <code>(trigger: Trigger) => void</code> | | | [triggers](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggers.md) | | <code>TriggerRegistry</code> | | | [triggerToActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggertoactions.md) | | <code>TriggerToActionsRegistry</code> | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md index bd340eb76fbac..6f03777e14552 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md @@ -7,5 +7,5 @@ <b>Signature:</b> ```typescript -readonly registerAction: <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; +readonly registerAction: <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; ``` diff --git a/docs/user/dashboard/download-underlying-data.asciidoc b/docs/user/dashboard/download-underlying-data.asciidoc new file mode 100644 index 0000000000000..78403ba797d78 --- /dev/null +++ b/docs/user/dashboard/download-underlying-data.asciidoc @@ -0,0 +1,15 @@ +[float] +[role="xpack"] +[[download_csv]] +=== Download CSV + +To download the underlying data of the Lens panels on your dashboard, you can use the *Download as CSV* option. + +TIP: The *Download as CSV* option supports multiple CSV file downloads from the same Lens visualization out of the box, if configured: for instance with multiple layers on a bar chart. + +To use the *Download as CSV* option: + +* Click the from the panel menu, then click *Download as CSV*. ++ +[role="screenshot"] +image::images/download_csv_context_menu.png[Download as CSV from panel context menu] \ No newline at end of file diff --git a/docs/user/dashboard/explore-dashboard-data.asciidoc b/docs/user/dashboard/explore-dashboard-data.asciidoc index 238dfb79e900b..66f91dc2bc18c 100644 --- a/docs/user/dashboard/explore-dashboard-data.asciidoc +++ b/docs/user/dashboard/explore-dashboard-data.asciidoc @@ -16,3 +16,4 @@ The data that displays depends on the element that you inspect. image:images/Dashboard_inspect.png[Inspect in dashboard] include::explore-underlying-data.asciidoc[] +include::download-underlying-data.asciidoc[] diff --git a/docs/user/dashboard/images/download_csv_context_menu.png b/docs/user/dashboard/images/download_csv_context_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..09f82b781249579c7f5ee92a111059112ad3b4d1 GIT binary patch literal 153535 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 YqU3&+q=eyY0|74$72P|?+mD|7KgY+xmH+?% literal 0 HcmV?d00001 diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index bd19a9f0d9cd3..b5451203e2365 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -9,9 +9,10 @@ "urlForwarding", "navigation", "uiActions", - "savedObjects" + "savedObjects", + "share" ], - "optionalPlugins": ["home", "share", "usageCollection", "savedObjectsTaggingOss"], + "optionalPlugins": ["home", "usageCollection", "savedObjectsTaggingOss"], "server": true, "ui": true, "requiredBundles": ["kibanaUtils", "kibanaReact", "home"] diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx new file mode 100644 index 0000000000000..770e01d6190cb --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart } from 'kibana/public'; + +import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '../../embeddable_plugin'; +import { DashboardContainer } from '../../application/embeddable'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../../application/test_helpers'; +import { + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardExportableEmbeddableFactory, + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; +import { ExportCSVAction } from './export_csv_action'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { DataPublicPluginStart } from '../../../../data/public/types'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { LINE_FEED_CHARACTER } from 'src/plugins/data/common/exports/export_csv'; + +describe('Export CSV action', () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( + CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + new ContactCardExportableEmbeddableFactory((() => null) as any, {} as any) + ); + const start = doStart(); + + let container: DashboardContainer; + let embeddable: ContactCardEmbeddable; + let coreStart: CoreStart; + let dataMock: jest.Mocked<DataPublicPluginStart>; + + beforeEach(async () => { + coreStart = coreMock.createStart(); + coreStart.savedObjects.client = { + ...coreStart.savedObjects.client, + get: jest.fn().mockImplementation(() => ({ attributes: { title: 'Holy moly' } })), + find: jest.fn().mockImplementation(() => ({ total: 15 })), + create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })), + }; + + const options = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + const input = getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel<ContactCardEmbeddableInput>({ + explicitInput: { firstName: 'Kibanana', id: '123' }, + type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + }), + }, + }); + container = new DashboardContainer(input, options); + dataMock = dataPluginMock.createStartContract(); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EXPORTABLE_EMBEDDABLE, { + firstName: 'Kibana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } else { + embeddable = contactCardEmbeddable; + } + }); + + test('Download is incompatible with embeddables without getInspectorAdapters implementation', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); + }); + + test('Should download a compatible Embeddable', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const result = ((await action.execute({ embeddable, asString: true })) as unknown) as + | undefined + | Record<string, { content: string; type: string }>; + expect(result).toEqual({ + 'Hello Kibana.csv': { + content: `First Name,Last Name${LINE_FEED_CHARACTER}Kibana,undefined${LINE_FEED_CHARACTER}`, + type: 'text/plain;charset=utf-8', + }, + }); + }); + + test('Should not download incompatible Embeddable', async () => { + const action = new ExportCSVAction({ core: coreStart, data: dataMock }); + const errorEmbeddable = new ErrorEmbeddable( + 'Wow what an awful error', + { id: ' 404' }, + embeddable.getRoot() as IContainer + ); + const result = ((await action.execute({ + embeddable: errorEmbeddable, + asString: true, + })) as unknown) as undefined | Record<string, string>; + expect(result).toBeUndefined(); + }); +}); diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx new file mode 100644 index 0000000000000..48a7877f9383e --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.tsx @@ -0,0 +1,138 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Datatable } from 'src/plugins/expressions/public'; +import { FormatFactory } from '../../../../data/common/field_formats/utils'; +import { DataPublicPluginStart, exporters } from '../../../../data/public'; +import { downloadMultipleAs } from '../../../../share/public'; +import { Adapters, IEmbeddable } from '../../../../embeddable/public'; +import { ActionByType } from '../../../../ui_actions/public'; +import { CoreStart } from '../../../../../core/public'; + +export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV'; + +export interface Params { + core: CoreStart; + data: DataPublicPluginStart; +} + +export interface ExportContext { + embeddable?: IEmbeddable; + // used for testing + asString?: boolean; +} + +/** + * This is "Export CSV" action which appears in the context + * menu of a dashboard panel. + */ +export class ExportCSVAction implements ActionByType<typeof ACTION_EXPORT_CSV> { + public readonly id = ACTION_EXPORT_CSV; + + public readonly type = ACTION_EXPORT_CSV; + + public readonly order = 5; + + constructor(protected readonly params: Params) {} + + public getIconType() { + return 'exportAction'; + } + + public readonly getDisplayName = (context: ExportContext): string => + i18n.translate('dashboard.actions.DownloadCreateDrilldownAction.displayName', { + defaultMessage: 'Download as CSV', + }); + + public async isCompatible(context: ExportContext): Promise<boolean> { + return !!this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.()); + } + + private hasDatatableContent = (adapters: Adapters | undefined) => { + return Object.keys(adapters?.tables || {}).length > 0; + }; + + private getFormatter = (): FormatFactory | undefined => { + if (this.params.data) { + return this.params.data.fieldFormats.deserialize; + } + }; + + private getDataTableContent = (adapters: Adapters | undefined) => { + if (this.hasDatatableContent(adapters)) { + return adapters?.tables; + } + return; + }; + + private exportCSV = async (context: ExportContext) => { + const formatFactory = this.getFormatter(); + // early exit if not formatter is available + if (!formatFactory) { + return; + } + const tableAdapters = this.getDataTableContent( + context?.embeddable?.getInspectorAdapters() + ) as Record<string, Datatable>; + + if (tableAdapters) { + const datatables = Object.values(tableAdapters); + const content = datatables.reduce<Record<string, { content: string; type: string }>>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + const untitledFilename = i18n.translate( + 'dashboard.actions.downloadOptionsUnsavedFilename', + { + defaultMessage: 'unsaved', + } + ); + + memo[`${context!.embeddable!.getTitle() || untitledFilename}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: this.params.core.uiSettings.get('csv:separator', ','), + quoteValues: this.params.core.uiSettings.get('csv:quoteValues', true), + formatFactory, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); + + // useful for testing + if (context.asString) { + return (content as unknown) as Promise<void>; + } + + if (content) { + return downloadMultipleAs(content); + } + } + }; + + public async execute(context: ExportContext): Promise<void> { + // make it testable: type here will be forced + return await this.exportCSV(context); + } +} diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index cd32c2025456f..3d7ebe76cb66a 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -47,3 +47,4 @@ export { LibraryNotificationAction, ACTION_LIBRARY_NOTIFICATION, } from './library_notification_action'; +export { ExportContext, ExportCSVAction, ACTION_EXPORT_CSV } from './export_csv_action'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index c47a4c2d21b11..76b1ccc037e89 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -101,6 +101,11 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; +import { + ACTION_EXPORT_CSV, + ExportContext, + ExportCSVAction, +} from './application/actions/export_csv_action'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -160,6 +165,7 @@ declare module '../../../plugins/ui_actions/public' { [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; + [ACTION_EXPORT_CSV]: ExportContext; } } @@ -414,7 +420,7 @@ export class DashboardPlugin public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; - const { uiActions } = plugins; + const { uiActions, data, share } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -431,6 +437,11 @@ export class DashboardPlugin uiActions.registerAction(clonePanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); + if (share) { + const ExportCSVPlugin = new ExportCSVAction({ core, data }); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); + } + if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); diff --git a/src/plugins/data/common/exports/export_csv.tsx b/src/plugins/data/common/exports/export_csv.tsx index 1e1420c245eb4..116586c5b66e8 100644 --- a/src/plugins/data/common/exports/export_csv.tsx +++ b/src/plugins/data/common/exports/export_csv.tsx @@ -22,7 +22,7 @@ import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; import { Datatable } from 'src/plugins/expressions'; -const LINE_FEED_CHARACTER = '\r\n'; +export const LINE_FEED_CHARACTER = '\r\n'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; const allDoubleQuoteRE = /"/g; export const CSV_MIME_TYPE = 'text/plain;charset=utf-8'; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx new file mode 100644 index 0000000000000..338eb4877a50a --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable.tsx @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ContactCardEmbeddable } from './contact_card_embeddable'; + +export class ContactCardExportableEmbeddable extends ContactCardEmbeddable { + public getInspectorAdapters = () => { + return { + tables: { + layer1: { + type: 'datatable', + columns: [ + { id: 'firstName', name: 'First Name' }, + { id: 'originalLastName', name: 'Last Name' }, + ], + rows: [ + { + firstName: this.getInput().firstName, + orignialLastName: this.getInput().lastName, + }, + ], + }, + }, + }; + }; +} + +export const CONTACT_EXPORTABLE_USER_TRIGGER = 'CONTACT_EXPORTABLE_USER_TRIGGER'; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx new file mode 100644 index 0000000000000..5b8827ac6fc2a --- /dev/null +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_exportable_embeddable_factory.tsx @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; + +import { CoreStart } from 'src/core/public'; +import { toMountPoint } from '../../../../../../kibana_react/public'; +import { EmbeddableFactoryDefinition } from '../../../embeddables'; +import { Container } from '../../../containers'; +import { ContactCardEmbeddableInput } from './contact_card_embeddable'; +import { ContactCardExportableEmbeddable } from './contact_card_exportable_embeddable'; +import { ContactCardInitializer } from './contact_card_initializer'; + +export const CONTACT_CARD_EXPORTABLE_EMBEDDABLE = 'CONTACT_CARD_EXPORTABLE_EMBEDDABLE'; + +export class ContactCardExportableEmbeddableFactory + implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput> { + public readonly type = CONTACT_CARD_EXPORTABLE_EMBEDDABLE; + + constructor( + private readonly execTrigger: UiActionsStart['executeTriggerActions'], + private readonly overlays: CoreStart['overlays'] + ) {} + + public async isEditable() { + return true; + } + + public getDisplayName() { + return i18n.translate('embeddableApi.samples.contactCard.displayName', { + defaultMessage: 'contact card', + }); + } + + public getExplicitInput = (): Promise<Partial<ContactCardEmbeddableInput>> => { + return new Promise((resolve) => { + const modalSession = this.overlays.openModal( + toMountPoint( + <ContactCardInitializer + onCancel={() => { + modalSession.close(); + // @ts-expect-error + resolve(undefined); + }} + onCreate={(input: { firstName: string; lastName?: string }) => { + modalSession.close(); + resolve(input); + }} + /> + ), + { + 'data-test-subj': 'createContactCardEmbeddable', + } + ); + }); + }; + + public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => { + return new ContactCardExportableEmbeddable( + initialInput, + { + execAction: this.execTrigger, + }, + parent + ); + }; +} diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts index c79a4f517916e..a9006cdc7b477 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/index.ts @@ -20,5 +20,7 @@ export * from './contact_card'; export * from './contact_card_embeddable'; export * from './contact_card_embeddable_factory'; +export * from './contact_card_exportable_embeddable'; +export * from './contact_card_exportable_embeddable_factory'; export * from './contact_card_initializer'; export * from './slow_contact_card_embeddable_factory'; diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 3a14f49169e09..ca27e19b247c2 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -234,7 +234,7 @@ export class UiActionsService { // // (undocumented) protected readonly actions: ActionRegistry; - readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: UiActionsActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">) => void; + readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: UiActionsActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void; // (undocumented) readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void; readonly clear: () => void; @@ -248,21 +248,21 @@ export class UiActionsService { readonly executionService: UiActionsExecutionService; readonly fork: () => UiActionsService; // (undocumented) - readonly getAction: <T extends UiActionsActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; + readonly getAction: <T extends UiActionsActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>; // (undocumented) - readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]; + readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]; // (undocumented) - readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">[]>; + readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly registerAction: <A extends UiActionsActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION">; + readonly registerAction: <A extends UiActionsActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">; // (undocumented) readonly registerTrigger: (trigger: Trigger) => void; // Warning: (ae-forgotten-export) The symbol "TriggerRegistry" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 76276f8b4c828..56d471be63d3e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -44,6 +44,7 @@ import { IndexPatternsContract } from '../../../../../../src/plugins/data/public import { getEditPath, DOC_TYPE } from '../../../common'; import { IBasePath } from '../../../../../../src/core/public'; import { LensAttributeService } from '../../lens_attribute_service'; +import { LensInspectorAdapters } from '../types'; export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>; @@ -84,6 +85,7 @@ export class Embeddable private subscription: Subscription; private autoRefreshFetchSubscription: Subscription; private isInitialized = false; + private activeData: LensInspectorAdapters | undefined; private externalSearchContext: { timeRange?: TimeRange; @@ -131,6 +133,10 @@ export class Embeddable } } + public getInspectorAdapters() { + return this.activeData; + } + async initializeSavedVis(input: LensEmbeddableInput) { const attributes: | LensSavedObjectAttributes @@ -175,6 +181,13 @@ export class Embeddable } } + private updateActiveData = ( + data: unknown, + inspectorAdapters?: LensInspectorAdapters | undefined + ) => { + this.activeData = inspectorAdapters; + }; + /** * * @param {HTMLElement} domNode @@ -194,6 +207,7 @@ export class Embeddable variables={input.palette ? { theme: { palette: input.palette } } : {}} searchSessionId={this.input.searchSessionId} handleEvent={this.handleEvent} + onData$={this.updateActiveData} renderMode={input.renderMode} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index d18372246b0e6..4645420898314 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -15,6 +15,7 @@ import { import { ExecutionContextSearch } from 'src/plugins/data/public'; import { RenderMode } from 'src/plugins/expressions'; import { getOriginalRequestErrorMessage } from '../error_helper'; +import { LensInspectorAdapters } from '../types'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -23,6 +24,7 @@ export interface ExpressionWrapperProps { searchContext: ExecutionContextSearch; searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; + onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void; renderMode?: RenderMode; } @@ -33,6 +35,7 @@ export function ExpressionWrapper({ variables, handleEvent, searchSessionId, + onData$, renderMode, }: ExpressionWrapperProps) { return ( @@ -60,6 +63,7 @@ export function ExpressionWrapper({ expression={expression} searchContext={searchContext} searchSessionId={searchSessionId} + onData$={onData$} renderMode={renderMode} renderError={(errorMessage, error) => ( <div data-test-subj="expression-renderer-error"> diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 2f9310ee24ae9..24075facb68eb 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -5,13 +5,16 @@ */ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; -import { DashboardStart } from 'src/plugins/dashboard/public'; -import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; -import { VisualizationsSetup, VisualizationsStart } from 'src/plugins/visualizations/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { UrlForwardingSetup } from 'src/plugins/url_forwarding/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../src/plugins/expressions/public'; +import { + VisualizationsSetup, + VisualizationsStart, +} from '../../../../src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; import { GlobalSearchPluginSetup } from '../../global_search/public'; import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { EditorFrameService } from './editor_frame_service'; @@ -64,6 +67,7 @@ export interface LensPluginStartDependencies { charts: ChartsPluginStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; } + export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 17b70b8510f04..c332d05039255 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -140,5 +140,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hasGeoSrcFilter = await filterBar.hasFilter('geo.src', 'US', true, true); expect(hasGeoSrcFilter).to.be(true); }); + + it('CSV export action exists in panel context menu', async () => { + const ACTION_ID = 'ACTION_EXPORT_CSV'; + const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + await panelActions.openContextMenu(); + await panelActions.clickContextMenuMoreItem(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); }); } From ae463cf1d7dfdcc0f8f0454a03e0e18e3f777a97 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm <matthias.wilhelm@elastic.co> Date: Thu, 3 Dec 2020 11:13:55 +0100 Subject: [PATCH 089/107] [Discover] Refactor getContextUrl to separate file (#84503) --- .../angular/doc_table/components/table_row.ts | 32 +++--------- .../helpers/get_context_url.test.ts | 41 +++++++++++++++ .../application/helpers/get_context_url.tsx | 52 +++++++++++++++++++ 3 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 src/plugins/discover/public/application/helpers/get_context_url.test.ts create mode 100644 src/plugins/discover/public/application/helpers/get_context_url.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 17f3199b75b15..e45f18606e3fc 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -18,20 +18,15 @@ */ import { find, template } from 'lodash'; -import { stringify } from 'query-string'; import $ from 'jquery'; -import rison from 'rison-node'; -import '../../doc_viewer'; - import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; - -import { dispatchRenderComplete, url } from '../../../../../../kibana_utils/public'; +import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; import cellTemplateHtml from '../components/table_row/cell.html'; import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; -import { esFilters } from '../../../../../../data/public'; import { getServices } from '../../../../kibana_services'; +import { getContextUrl } from '../../../helpers/get_context_url'; const TAGS_WITH_WS = />\s+</g; @@ -115,25 +110,12 @@ export function createTableRowDirective($compile: ng.ICompileService) { }; $scope.getContextAppHref = () => { - const globalFilters: any = getServices().filterManager.getGlobalFilters(); - const appFilters: any = getServices().filterManager.getAppFilters(); - - const hash = stringify( - url.encodeQuery({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns: $scope.columns, - filters: (appFilters || []).map(esFilters.disableFilter), - }), - }), - { encode: false, sort: false } + return getContextUrl( + $scope.row._id, + $scope.indexPattern.id, + $scope.columns, + getServices().filterManager ); - - return `#/context/${encodeURIComponent($scope.indexPattern.id)}/${encodeURIComponent( - $scope.row._id - )}?${hash}`; }; // create a tr element that lists the value for each *column* diff --git a/src/plugins/discover/public/application/helpers/get_context_url.test.ts b/src/plugins/discover/public/application/helpers/get_context_url.test.ts new file mode 100644 index 0000000000000..481ea6b1a5b4f --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_context_url.test.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getContextUrl } from './get_context_url'; +import { FilterManager } from '../../../../data/public/query/filter_manager'; +const filterManager = ({ + getGlobalFilters: () => [], + getAppFilters: () => [], +} as unknown) as FilterManager; + +describe('Get context url', () => { + test('returning a valid context url', async () => { + const url = await getContextUrl('docId', 'ipId', ['test1', 'test2'], filterManager); + expect(url).toMatchInlineSnapshot( + `"#/context/ipId/docId?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` + ); + }); + + test('returning a valid context url when docId contains whitespace', async () => { + const url = await getContextUrl('doc Id', 'ipId', ['test1', 'test2'], filterManager); + expect(url).toMatchInlineSnapshot( + `"#/context/ipId/doc%20Id?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` + ); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_context_url.tsx b/src/plugins/discover/public/application/helpers/get_context_url.tsx new file mode 100644 index 0000000000000..b159341cbe28d --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_context_url.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import { url } from '../../../../kibana_utils/common'; +import { esFilters, FilterManager } from '../../../../data/public'; + +/** + * Helper function to generate an URL to a document in Discover's context view + */ +export function getContextUrl( + documentId: string, + indexPatternId: string, + columns: string[], + filterManager: FilterManager +) { + const globalFilters = filterManager.getGlobalFilters(); + const appFilters = filterManager.getAppFilters(); + + const hash = stringify( + url.encodeQuery({ + _g: rison.encode({ + filters: globalFilters || [], + }), + _a: rison.encode({ + columns, + filters: (appFilters || []).map(esFilters.disableFilter), + }), + }), + { encode: false, sort: false } + ); + + return `#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( + documentId + )}?${hash}`; +} From bec33426ebddfd9e3f393247f13afd2a190db120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 3 Dec 2020 11:21:04 +0100 Subject: [PATCH 090/107] Fixed a11y issue on rollup jobs table selection (#84567) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../crud_app/sections/job_list/job_table/job_table.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 66ecb37d68439..0fd7f62511bdb 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -310,6 +310,10 @@ export class JobTable extends Component { this.toggleItem(id); }} data-test-subj={`indexTableRowCheckbox-${id}`} + aria-label={i18n.translate('xpack.rollupJobs.jobTable.selectRow', { + defaultMessage: 'Select this row {id}', + values: { id }, + })} /> </EuiTableRowCellCheckbox> @@ -380,6 +384,9 @@ export class JobTable extends Component { checked={this.areAllItemsSelected()} onChange={this.toggleAll} type="inList" + aria-label={i18n.translate('xpack.rollupJobs.jobTable.selectAllRows', { + defaultMessage: 'Select all rows', + })} /> </EuiTableHeaderCellCheckbox> {this.buildHeader()} From f5fb14f07aefd15c3577aba8e4023592f4edc8f8 Mon Sep 17 00:00:00 2001 From: eyalkoren <41850454+eyalkoren@users.noreply.github.com> Date: Thu, 3 Dec 2020 12:22:38 +0200 Subject: [PATCH 091/107] [APM] Add APM agent config options (#84678) --- .../runtime_types/log_level_rt.ts | 17 ++++++ .../__snapshots__/index.test.ts.snap | 44 ++++++++++++++- .../setting_definitions/general_settings.ts | 53 ++++++++++++++++++- .../setting_definitions/index.test.ts | 3 ++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts new file mode 100644 index 0000000000000..b488faa8e8fdc --- /dev/null +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/log_level_rt.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const logLevelRt = t.union([ + t.literal('trace'), + t.literal('debug'), + t.literal('info'), + t.literal('warning'), + t.literal('error'), + t.literal('critical'), + t.literal('off'), +]); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 2962a5fd2df3b..fc42af5ff7724 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -64,8 +64,38 @@ Array [ }, Object { "key": "log_level", - "type": "text", - "validationName": "string", + "options": Array [ + Object { + "text": "trace", + "value": "trace", + }, + Object { + "text": "debug", + "value": "debug", + }, + Object { + "text": "info", + "value": "info", + }, + Object { + "text": "warning", + "value": "warning", + }, + Object { + "text": "error", + "value": "error", + }, + Object { + "text": "critical", + "value": "critical", + }, + Object { + "text": "off", + "value": "off", + }, + ], + "type": "select", + "validationName": "(\\"trace\\" | \\"debug\\" | \\"info\\" | \\"warning\\" | \\"error\\" | \\"critical\\" | \\"off\\")", }, Object { "key": "profiling_inferred_spans_enabled", @@ -110,6 +140,11 @@ Array [ "type": "boolean", "validationName": "(\\"true\\" | \\"false\\")", }, + Object { + "key": "sanitize_field_names", + "type": "text", + "validationName": "string", + }, Object { "key": "server_timeout", "min": "1ms", @@ -170,6 +205,11 @@ Array [ "type": "float", "validationName": "floatRt", }, + Object { + "key": "transaction_ignore_urls", + "type": "text", + "validationName": "string", + }, Object { "key": "transaction_max_spans", "max": undefined, diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index e777e1fd09d0b..59a315830aec5 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { captureBodyRt } from '../runtime_types/capture_body_rt'; +import { logLevelRt } from '../runtime_types/log_level_rt'; import { RawSettingDefinition } from './types'; export const generalSettings: RawSettingDefinition[] = [ @@ -91,7 +92,8 @@ export const generalSettings: RawSettingDefinition[] = [ // LOG_LEVEL { key: 'log_level', - type: 'text', + validation: logLevelRt, + type: 'select', defaultValue: 'info', label: i18n.translate('xpack.apm.agentConfig.logLevel.label', { defaultMessage: 'Log level', @@ -99,7 +101,16 @@ export const generalSettings: RawSettingDefinition[] = [ description: i18n.translate('xpack.apm.agentConfig.logLevel.description', { defaultMessage: 'Sets the logging level for the agent', }), - includeAgents: ['dotnet', 'ruby'], + options: [ + { text: 'trace', value: 'trace' }, + { text: 'debug', value: 'debug' }, + { text: 'info', value: 'info' }, + { text: 'warning', value: 'warning' }, + { text: 'error', value: 'error' }, + { text: 'critical', value: 'critical' }, + { text: 'off', value: 'off' }, + ], + includeAgents: ['dotnet', 'ruby', 'java'], }, // Recording @@ -207,4 +218,42 @@ export const generalSettings: RawSettingDefinition[] = [ } ), }, + + // Sanitize field names + { + key: 'sanitize_field_names', + type: 'text', + defaultValue: + 'password, passwd, pwd, secret, *key, *token*, *session*, *credit*, *card*, authorization, set-cookie', + label: i18n.translate('xpack.apm.agentConfig.sanitizeFiledNames.label', { + defaultMessage: 'Sanitize field names', + }), + description: i18n.translate( + 'xpack.apm.agentConfig.sanitizeFiledNames.description', + { + defaultMessage: + 'Sometimes it is necessary to sanitize, i.e., remove, sensitive data sent to Elastic APM. This config accepts a list of wildcard patterns of field names which should be sanitized. These apply to HTTP headers (including cookies) and `application/x-www-form-urlencoded` data (POST form fields). The query string and the captured request body (such as `application/json` data) will not get sanitized.', + } + ), + includeAgents: ['java'], + }, + + // Ignore transactions based on URLs + { + key: 'transaction_ignore_urls', + type: 'text', + defaultValue: + 'Agent specific - check out the documentation of this config option in the corresponding agent documentation.', + label: i18n.translate('xpack.apm.agentConfig.transactionIgnoreUrl.label', { + defaultMessage: 'Ignore transactions based on URLs', + }), + description: i18n.translate( + 'xpack.apm.agentConfig.transactionIgnoreUrl.description', + { + defaultMessage: + 'Used to restrict requests to certain URLs from being instrumented. This config accepts a comma-separated list of wildcard patterns of URL paths that should be ignored. When an incoming HTTP request is detected, its request path will be tested against each element in this list. For example, adding `/home/index` to this list would match and remove instrumentation from `http://localhost/home/index` as well as `http://whatever.com/home/index?value1=123`', + } + ), + includeAgents: ['java'], + }, ]; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index 1f247813104ec..a00f1ab5bb4d1 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -61,12 +61,14 @@ describe('filterByAgent', () => { 'capture_headers', 'circuit_breaker_enabled', 'enable_log_correlation', + 'log_level', 'profiling_inferred_spans_enabled', 'profiling_inferred_spans_excluded_classes', 'profiling_inferred_spans_included_classes', 'profiling_inferred_spans_min_duration', 'profiling_inferred_spans_sampling_interval', 'recording', + 'sanitize_field_names', 'server_timeout', 'span_frames_min_duration', 'stack_trace_limit', @@ -75,6 +77,7 @@ describe('filterByAgent', () => { 'stress_monitor_gc_stress_threshold', 'stress_monitor_system_cpu_relief_threshold', 'stress_monitor_system_cpu_stress_threshold', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); From 37e907078c503aeb3e642ec674f98d59aee52607 Mon Sep 17 00:00:00 2001 From: MadameSheema <snootchie.boochies@gmail.com> Date: Thu, 3 Dec 2020 11:48:40 +0100 Subject: [PATCH 092/107] [Security Solution][Detections] Implements indicator match rule cypress test (#84323) * implemnts indicator match rule cypress test * fixes merge issue * fixes type check issues * fixes mapping * simplifies data * fixes excpetions flakiness * fixes alerts test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cypress/integration/alerts.spec.ts | 36 +- .../alerts_detection_exceptions.spec.ts | 29 +- ...ts_detection_rules_indicator_match.spec.ts | 197 + .../security_solution/cypress/objects/rule.ts | 53 +- .../cypress/screens/alerts.ts | 4 +- .../cypress/screens/create_new_rule.ts | 6 + .../cypress/screens/rule_details.ts | 6 + .../cypress/tasks/create_new_rule.ts | 71 +- .../es_archives/threat_data/data.json.gz | Bin 0 -> 1086 bytes .../es_archives/threat_data/mappings.json | 3577 +++++++++++++++++ .../es_archives/threat_indicator/data.json | 13 + .../threat_indicator/mappings.json | 30 + 12 files changed, 3954 insertions(+), 68 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 8e3b30cddd121..0810babc9370b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { - NUMBER_OF_ALERTS, + ALERTS, + ALERTS_COUNT, SELECTED_ALERTS, SHOWING_ALERTS, - ALERTS, TAKE_ACTION_POPOVER_BTN, } from '../screens/alerts'; @@ -45,7 +45,7 @@ describe('Alerts', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { cy.get(SHOWING_ALERTS).should('have.text', `Showing ${numberOfAlerts} alerts`); @@ -64,10 +64,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; - cy.get(NUMBER_OF_ALERTS).should( - 'have.text', - expectedNumberOfAlertsAfterClosing.toString() - ); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlertsAfterClosing.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', @@ -77,7 +74,7 @@ describe('Alerts', () => { goToClosedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeClosed.toString()); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeClosed.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeClosed.toString()} alerts` @@ -98,7 +95,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfClosedAlertsAfterOpened = 2; - cy.get(NUMBER_OF_ALERTS).should( + cy.get(ALERTS_COUNT).should( 'have.text', expectedNumberOfClosedAlertsAfterOpened.toString() ); @@ -128,7 +125,7 @@ describe('Alerts', () => { it('Closes one alert when more than one opened alerts are selected', () => { waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { const numberOfAlertsToBeClosed = 1; @@ -144,7 +141,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeClosed; - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${expectedNumberOfAlerts.toString()} alerts` @@ -153,7 +150,7 @@ describe('Alerts', () => { goToClosedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeClosed.toString()); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeClosed.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeClosed.toString()} alert` @@ -178,7 +175,7 @@ describe('Alerts', () => { goToClosedAlerts(); waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { const numberOfAlertsToBeOpened = 1; @@ -195,7 +192,7 @@ describe('Alerts', () => { waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeOpened; - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${expectedNumberOfAlerts.toString()} alerts` @@ -204,7 +201,7 @@ describe('Alerts', () => { goToOpenedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeOpened.toString()); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeOpened.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeOpened.toString()} alert` @@ -228,7 +225,7 @@ describe('Alerts', () => { waitForAlerts(); waitForAlertsToBeLoaded(); - cy.get(NUMBER_OF_ALERTS) + cy.get(ALERTS_COUNT) .invoke('text') .then((numberOfAlerts) => { const numberOfAlertsToBeMarkedInProgress = 1; @@ -244,7 +241,7 @@ describe('Alerts', () => { waitForAlertsToBeLoaded(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedInProgress; - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${expectedNumberOfAlerts.toString()} alerts` @@ -253,10 +250,7 @@ describe('Alerts', () => { goToInProgressAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS).should( - 'have.text', - numberOfAlertsToBeMarkedInProgress.toString() - ); + cy.get(ALERTS_COUNT).should('have.text', numberOfAlertsToBeMarkedInProgress.toString()); cy.get(SHOWING_ALERTS).should( 'have.text', `Showing ${numberOfAlertsToBeMarkedInProgress.toString()} alert` diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts index b1d7163ac70e0..160dbad9a06be 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_exceptions.spec.ts @@ -6,8 +6,8 @@ import { exception } from '../objects/exception'; import { newRule } from '../objects/rule'; +import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../screens/alerts'; import { RULE_STATUS } from '../screens/create_new_rule'; -import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { addExceptionFromFirstAlert, @@ -52,7 +52,8 @@ describe('Exceptions', () => { waitForAlertsToPopulate(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfInitialAlertsText) => { cy.wrap(parseInt(numberOfInitialAlertsText, 10)).should( @@ -77,7 +78,8 @@ describe('Exceptions', () => { goToAlertsTab(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -86,7 +88,8 @@ describe('Exceptions', () => { goToClosedAlerts(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfClosedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfClosedAlertsAfterCreatingExceptionText, 10)).should( @@ -99,7 +102,8 @@ describe('Exceptions', () => { waitForTheRuleToBeExecuted(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfOpenedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfOpenedAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -113,7 +117,8 @@ describe('Exceptions', () => { waitForAlertsToPopulate(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterRemovingExceptionsText) => { cy.wrap(parseInt(numberOfAlertsAfterRemovingExceptionsText, 10)).should( @@ -130,7 +135,8 @@ describe('Exceptions', () => { addsException(exception); esArchiverLoad('auditbeat_for_exceptions2'); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -139,7 +145,8 @@ describe('Exceptions', () => { goToClosedAlerts(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfClosedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfClosedAlertsAfterCreatingExceptionText, 10)).should( @@ -152,7 +159,8 @@ describe('Exceptions', () => { waitForTheRuleToBeExecuted(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfOpenedAlertsAfterCreatingExceptionText) => { cy.wrap(parseInt(numberOfOpenedAlertsAfterCreatingExceptionText, 10)).should('eql', 0); @@ -165,7 +173,8 @@ describe('Exceptions', () => { waitForAlertsToPopulate(); refreshPage(); - cy.get(SERVER_SIDE_EVENT_COUNT) + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS) .invoke('text') .then((numberOfAlertsAfterRemovingExceptionsText) => { cy.wrap(parseInt(numberOfAlertsAfterRemovingExceptionsText, 10)).should( diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts new file mode 100644 index 0000000000000..03e714f2381c6 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_indicator_match.spec.ts @@ -0,0 +1,197 @@ +/* + * 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 { newThreatIndicatorRule } from '../objects/rule'; + +import { + ALERT_RULE_METHOD, + ALERT_RULE_NAME, + ALERT_RULE_RISK_SCORE, + ALERT_RULE_SEVERITY, + ALERT_RULE_VERSION, + NUMBER_OF_ALERTS, +} from '../screens/alerts'; +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + RULE_SWITCH, + SEVERITY, +} from '../screens/alerts_detection_rules'; +import { + ABOUT_DETAILS, + ABOUT_INVESTIGATION_NOTES, + ABOUT_RULE_DESCRIPTION, + ADDITIONAL_LOOK_BACK_DETAILS, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, + INDICATOR_INDEX_PATTERNS, + INDICATOR_INDEX_QUERY, + INDICATOR_MAPPING, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + removeExternalLinkText, + RISK_SCORE_DETAILS, + RULE_NAME_HEADER, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, + SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, +} from '../screens/rule_details'; + +import { + goToManageAlertsDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../tasks/alerts'; +import { + changeToThreeHundredRowsPerPage, + deleteRule, + filterByCustomRules, + goToCreateNewRule, + goToRuleDetails, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/alerts_detection_rules'; +import { removeSignalsIndex } from '../tasks/api_calls'; +import { + createAndActivateRule, + fillAboutRuleAndContinue, + fillDefineIndicatorMatchRuleAndContinue, + fillScheduleRuleAndContinue, + selectIndicatorMatchType, + waitForAlertsToPopulate, + waitForTheRuleToBeExecuted, +} from '../tasks/create_new_rule'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS_URL } from '../urls/navigation'; + +const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); +const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); +const expectedTags = newThreatIndicatorRule.tags.join(''); +const expectedMitre = newThreatIndicatorRule.mitre + .map(function (mitre) { + return mitre.tactic + mitre.techniques.join(''); + }) + .join(''); +const expectedNumberOfRules = 1; +const expectedNumberOfAlerts = 1; + +describe('Detection rules, Indicator Match', () => { + beforeEach(() => { + esArchiverLoad('threat_indicator'); + esArchiverLoad('threat_data'); + }); + + afterEach(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('threat_data'); + removeSignalsIndex(); + deleteRule(); + }); + + it('Creates and activates a new Indicator Match rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectIndicatorMatchType(); + 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/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 8ba545e242b47..06046b9385712 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -30,10 +30,10 @@ interface Interval { } export interface CustomRule { - customQuery: string; + customQuery?: string; name: string; description: string; - index?: string[]; + index: string[]; interval?: string; severity: string; riskScore: string; @@ -43,7 +43,7 @@ export interface CustomRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; - timelineId: string; + timelineId?: string; runsEvery: Interval; lookBack: Interval; } @@ -60,6 +60,12 @@ export interface OverrideRule extends CustomRule { timestampOverride: string; } +export interface ThreatIndicatorRule extends CustomRule { + indicatorIndexPattern: string[]; + indicatorMapping: string; + indicatorIndexField: string; +} + export interface MachineLearningRule { machineLearningJob: string; anomalyScoreThreshold: string; @@ -77,6 +83,16 @@ export interface MachineLearningRule { lookBack: Interval; } +export const indexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + const mitre1: Mitre = { tactic: 'Discovery (TA0007)', techniques: ['Cloud Service Discovery (T1526)', 'File and Directory Discovery (T1083)'], @@ -121,6 +137,7 @@ const lookBack: Interval = { export const newRule: CustomRule = { customQuery: 'host.name:*', + index: indexPatterns, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -162,6 +179,7 @@ export const existingRule: CustomRule = { export const newOverrideRule: OverrideRule = { customQuery: 'host.name:*', + index: indexPatterns, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -182,6 +200,7 @@ export const newOverrideRule: OverrideRule = { export const newThresholdRule: ThresholdRule = { customQuery: 'host.name:*', + index: indexPatterns, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -217,6 +236,7 @@ export const machineLearningRule: MachineLearningRule = { export const eqlRule: CustomRule = { customQuery: 'any where process.name == "which"', name: 'New EQL Rule', + index: indexPatterns, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -236,6 +256,7 @@ export const eqlSequenceRule: CustomRule = { [any where process.name == "which"]\ [any where process.name == "xargs"]', name: 'New EQL Sequence Rule', + index: indexPatterns, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -249,15 +270,23 @@ export const eqlSequenceRule: CustomRule = { lookBack, }; -export const indexPatterns = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', -]; +export const newThreatIndicatorRule: ThreatIndicatorRule = { + name: 'Threat Indicator Rule Test', + description: 'The threat indicator rule description.', + index: ['threat-data-*'], + severity: 'Critical', + riskScore: '20', + tags: ['test', 'threat'], + referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [mitre1, mitre2], + note: '# test markdown', + runsEvery, + lookBack, + indicatorIndexPattern: ['threat-indicator-*'], + indicatorMapping: 'agent.id', + indicatorIndexField: 'agent.threat', +}; export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical']; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 2c80d02cad83d..bc3be900284b4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -8,6 +8,8 @@ export const ADD_EXCEPTION_BTN = '[data-test-subj="addExceptionButton"]'; export const ALERTS = '[data-test-subj="event"]'; +export const ALERTS_COUNT = '[data-test-subj="server-side-event-count"]'; + export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; @@ -43,7 +45,7 @@ export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-st export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN = '[data-test-subj="markSelectedAlertsInProgressButton"]'; -export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; +export const NUMBER_OF_ALERTS = '[data-test-subj="local-events-count"]'; export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]'; 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 d802e97363a68..ab9347f1862cc 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 @@ -27,8 +27,12 @@ export const MITRE_BTN = '[data-test-subj="addMitre"]'; export const ADVANCED_SETTINGS_BTN = '[data-test-subj="advancedSettings"] .euiAccordion__button'; +export const COMBO_BOX_CLEAR_BTN = '[data-test-subj="comboBoxClearButton"]'; + export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]'; +export const COMBO_BOX_RESULT = '.euiFilterSelectItem'; + export const CREATE_AND_ACTIVATE_BTN = '[data-test-subj="create-activate"]'; export const CUSTOM_QUERY_INPUT = @@ -57,6 +61,8 @@ export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loa export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const INDICATOR_MATCH_TYPE = '[data-test-subj="threatMatchRuleType"]'; + export const INPUT = '[data-test-subj="input"]'; export const INVESTIGATION_NOTES_TEXTAREA = diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 8e93d5dcd6315..ad969b54ffd90 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -36,6 +36,12 @@ export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; +export const INDICATOR_INDEX_PATTERNS = 'Indicator index patterns'; + +export const INDICATOR_INDEX_QUERY = 'Indicator index query'; + +export const INDICATOR_MAPPING = 'Indicator mapping'; + export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; export const INVESTIGATION_NOTES_TOGGLE = '[data-test-subj="stepAboutDetailsToggle-notes"]'; 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 9b809dbe524ae..219c6496ee893 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 @@ -9,6 +9,7 @@ import { MachineLearningRule, machineLearningRule, OverrideRule, + ThreatIndicatorRule, ThresholdRule, } from '../objects/rule'; import { @@ -26,6 +27,7 @@ import { DEFINE_EDIT_TAB, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, + INDICATOR_MATCH_TYPE, INPUT, INVESTIGATION_NOTES_TEXTAREA, LOOK_BACK_INTERVAL, @@ -63,11 +65,13 @@ import { QUERY_PREVIEW_BUTTON, EQL_QUERY_PREVIEW_HISTOGRAM, EQL_QUERY_VALIDATION_SPINNER, + COMBO_BOX_CLEAR_BTN, + COMBO_BOX_RESULT, } from '../screens/create_new_rule'; -import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { NOTIFICATION_TOASTS, TOAST_ERROR_CLASS } from '../screens/shared'; import { TIMELINE } from '../screens/timelines'; import { refreshPage } from './security_header'; +import { NUMBER_OF_ALERTS } from '../screens/alerts'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); @@ -75,7 +79,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRule = (rule: CustomRule | MachineLearningRule | ThresholdRule) => { +export const fillAboutRule = ( + rule: CustomRule | MachineLearningRule | ThresholdRule | ThreatIndicatorRule +) => { cy.get(RULE_NAME_INPUT).clear({ force: true }).type(rule.name, { force: true }); cy.get(RULE_DESCRIPTION_INPUT).clear({ force: true }).type(rule.description, { force: true }); @@ -121,7 +127,7 @@ export const fillAboutRule = (rule: CustomRule | MachineLearningRule | Threshold }; export const fillAboutRuleAndContinue = ( - rule: CustomRule | MachineLearningRule | ThresholdRule + rule: CustomRule | MachineLearningRule | ThresholdRule | ThreatIndicatorRule ) => { fillAboutRule(rule); cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); @@ -195,7 +201,7 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( rule: CustomRule | OverrideRule ) => { cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); - cy.get(TIMELINE(rule.timelineId)).click(); + cy.get(TIMELINE(rule.timelineId!)).click(); cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); @@ -213,7 +219,7 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery!); cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) .find(INPUT) @@ -228,7 +234,7 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { }; export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { - cy.get(EQL_QUERY_INPUT).type(rule.customQuery); + cy.get(EQL_QUERY_INPUT).type(rule.customQuery!); cy.get(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); cy.get(QUERY_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true }); cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); @@ -238,6 +244,22 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { cy.get(EQL_QUERY_INPUT).should('not.exist'); }; +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 }); + cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); +}; + export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRule) => { cy.get(MACHINE_LEARNING_DROPDOWN).click({ force: true }); cy.contains(MACHINE_LEARNING_LIST, rule.machineLearningJob).click(); @@ -265,6 +287,14 @@ export const goToActionsStepTab = () => { cy.get(ACTIONS_EDIT_TAB).click({ force: true }); }; +export const selectEqlRuleType = () => { + cy.get(EQL_TYPE).click({ force: true }); +}; + +export const selectIndicatorMatchType = () => { + cy.get(INDICATOR_MATCH_TYPE).click({ force: true }); +}; + export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; @@ -273,22 +303,6 @@ export const selectThresholdRuleType = () => { cy.get(THRESHOLD_TYPE).click({ force: true }); }; -export const waitForAlertsToPopulate = async () => { - cy.waitUntil( - () => { - refreshPage(); - return cy - .get(SERVER_SIDE_EVENT_COUNT) - .invoke('text') - .then((countText) => { - const alertCount = parseInt(countText, 10) || 0; - return alertCount > 0; - }); - }, - { interval: 500, timeout: 12000 } - ); -}; - export const waitForTheRuleToBeExecuted = () => { cy.waitUntil(() => { cy.get(REFRESH_BUTTON).click(); @@ -299,6 +313,15 @@ export const waitForTheRuleToBeExecuted = () => { }); }; -export const selectEqlRuleType = () => { - cy.get(EQL_TYPE).click({ force: true }); +export const waitForAlertsToPopulate = async () => { + cy.waitUntil(() => { + refreshPage(); + return cy + .get(NUMBER_OF_ALERTS) + .invoke('text') + .then((countText) => { + const alertCount = parseInt(countText, 10) || 0; + return alertCount > 0; + }); + }); }; diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ab63f9a47a7baa9e6ea0e830f55ec8c5e48df4c9 GIT binary patch literal 1086 zcmV-E1i||siwFpR@w;CD17u-zVJ>QOZ*Bm+R!eW&I1s+)R|GzFfyyCeJq;|d2+~8b zD0*lX1+rKe6g9HBmPmo5?6^h#`;xNkkdlISFAah?o;PQ{`N;1#x3>#@YGJXyU6g_@ z-dn+e)SZ=lH($(GR$A=_o<5|_@&0rBl|3Bci@x8S&8-D5;n^DLodlwTl4uejgfDs} zI!Rw68p$7;HJ~(UTI&`foCnDK;zxwm5niKYiE#Qf_#1n&1+JX{Mg;8+8jz&koC{2F zM#DUTAdagvh;o90EJJC4(yTNJicpze0~-IGP@0pbKf3B9qqb-!j>I)Ohej((3q&EP zl9&cjnC1aVY{_wj%eW{ILWS#f=_u(+rVG;%S9t)bnBZ2QEzuG!2Gz^;u(W2A(-tQU z%7`N5R@ZkAqa_Y)s1Un(T0-}rtq*pkLfXiyEQ+$3#G)(xyyQSwO$t^secF5zygyf` z0%|HWy~lyyE^cPZy-_=D%u<sSCkV$;62>^mqu6maX7l5?TD&-!8bWuBPZC`^&v9TY zDTyqDa6UpS#lJxHe5p_qr5OzrgXT^511mvV>n&}kQ!EX>87KNY>zPr8GowuMWf(`x z;q#}*nW0H~pvq6{;0`bG9PZ#SfgPbk{R<BIDCB-q)#B*ZW#$B1Ay2N2K}J<E>Y7<f zP@wzi3VRYy*~v?v@+|ZE0=%4Oh<F*t@sk^6&(5~zW4KrmWmlhpbyC_4)X^D~^c{RV z!=z1p2fQv((X=&ao1!AM0w;cDAW$2*mK{tYZKdd1Kh!`tZi$zzkV5EbHI~LkGm~aD zw1IXfd%x!_*(4s8sNkBI?UC#olru&D4{C&@%5p@i6;4PP=OLnbj0uH`MoCdYwHRak zZ+F?|smrzvGPGVt<_=jKD6RXiIDXy4e|#z!$Bn|Z@kC^8+|b*eKRl!Gcc^b&2^Y7L zDJn*FlZMs(E|gF(!hP)?BnOLzMQoWrzzDAbOCGZ<_*c<!-TAt<Cb|-`=~;M2?=E@1 zMA7ZL2V3Or1LIrqo5N%i91XiWPgM!>(9<*>xA(yr0(iaMfBKUb=@<4jd`u7cll-u| zzf+$-Ztp(+?(I2~a3vJc=|Xg7WoJn)bgxrMxEh#lp}lrp37@rxXu2GRq$wyh-jA&s z1Lm$%@~&X~u083U;48nYSM64aZ4Dc9PtyHH?cum72{h(Bv+$z!F$4~OWkHxf;>1wP zI$jxqM;?E{Gtf?x;!IWJik9gdCyWa6df87W4dY2y6v#t=asBd3$!2Dw=fQP^136Ef z#;?a;^&A=s^1)-DbYoH&ZZuzNC%WxtfZmV9-K==tm~m}b!@P36`x`BNh+fHMV{6%v zvXp1sFVJ&kezGb`qGXjog3#D;sKyb#+>HNwZAz!c(D~Ub>*n(J<>uw)KU(UR5_${( E0C|!QdjJ3c literal 0 HcmV?d00001 diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json new file mode 100644 index 0000000000000..3ccdee6bdb5eb --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json @@ -0,0 +1,3577 @@ +{ + "type": "index", + "value": { + "aliases": { + "thread-data": { + "is_write_index": false + }, + "beats": { + }, + "siem-read-alias": { + } + }, + "index": "threat-data-001", + "mappings": { + "_meta": { + "beat": "auditbeat", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "auditd": { + "properties": { + "data": { + "properties": { + "a0": { + "ignore_above": 1024, + "type": "keyword" + }, + "a1": { + "ignore_above": 1024, + "type": "keyword" + }, + "a2": { + "ignore_above": 1024, + "type": "keyword" + }, + "a3": { + "ignore_above": 1024, + "type": "keyword" + }, + "a[0-3]": { + "ignore_above": 1024, + "type": "keyword" + }, + "acct": { + "ignore_above": 1024, + "type": "keyword" + }, + "acl": { + "ignore_above": 1024, + "type": "keyword" + }, + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "added": { + "ignore_above": 1024, + "type": "keyword" + }, + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "apparmor": { + "ignore_above": 1024, + "type": "keyword" + }, + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "argc": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_limit": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_backlog_wait_time": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "audit_failure": { + "ignore_above": 1024, + "type": "keyword" + }, + "banners": { + "ignore_above": 1024, + "type": "keyword" + }, + "bool": { + "ignore_above": 1024, + "type": "keyword" + }, + "bus": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "capability": { + "ignore_above": 1024, + "type": "keyword" + }, + "cgroup": { + "ignore_above": 1024, + "type": "keyword" + }, + "changed": { + "ignore_above": 1024, + "type": "keyword" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "cmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "compat": { + "ignore_above": 1024, + "type": "keyword" + }, + "daddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "default-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "dmac": { + "ignore_above": 1024, + "type": "keyword" + }, + "dport": { + "ignore_above": 1024, + "type": "keyword" + }, + "enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "entries": { + "ignore_above": 1024, + "type": "keyword" + }, + "exit": { + "ignore_above": 1024, + "type": "keyword" + }, + "fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "fd": { + "ignore_above": 1024, + "type": "keyword" + }, + "fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "feature": { + "ignore_above": 1024, + "type": "keyword" + }, + "fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "file": { + "ignore_above": 1024, + "type": "keyword" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "format": { + "ignore_above": 1024, + "type": "keyword" + }, + "fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "grantors": { + "ignore_above": 1024, + "type": "keyword" + }, + "grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "hook": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "icmp_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "igid": { + "ignore_above": 1024, + "type": "keyword" + }, + "img-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "info": { + "ignore_above": 1024, + "type": "keyword" + }, + "inif": { + "ignore_above": 1024, + "type": "keyword" + }, + "ino": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "invalid_context": { + "ignore_above": 1024, + "type": "keyword" + }, + "ioctlcmd": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ipx-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "items": { + "ignore_above": 1024, + "type": "keyword" + }, + "iuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "ksize": { + "ignore_above": 1024, + "type": "keyword" + }, + "laddr": { + "ignore_above": 1024, + "type": "keyword" + }, + "len": { + "ignore_above": 1024, + "type": "keyword" + }, + "list": { + "ignore_above": 1024, + "type": "keyword" + }, + "lport": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "macproto": { + "ignore_above": 1024, + "type": "keyword" + }, + "maj": { + "ignore_above": 1024, + "type": "keyword" + }, + "major": { + "ignore_above": 1024, + "type": "keyword" + }, + "minor": { + "ignore_above": 1024, + "type": "keyword" + }, + "model": { + "ignore_above": 1024, + "type": "keyword" + }, + "msg": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "nargs": { + "ignore_above": 1024, + "type": "keyword" + }, + "net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "new-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "new_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-fam": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-grp": { + "ignore_above": 1024, + "type": "keyword" + }, + "nlnk-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ocomm": { + "ignore_above": 1024, + "type": "keyword" + }, + "oflag": { + "ignore_above": 1024, + "type": "keyword" + }, + "old": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-auid": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-chardev": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-disk": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-fs": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-level": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-log_passwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-mem": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-net": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-range": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-rng": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-role": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "old-vcpu": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_enforcing": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_lock": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pa": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "old_val": { + "ignore_above": 1024, + "type": "keyword" + }, + "op": { + "ignore_above": 1024, + "type": "keyword" + }, + "operation": { + "ignore_above": 1024, + "type": "keyword" + }, + "opid": { + "ignore_above": 1024, + "type": "keyword" + }, + "oses": { + "ignore_above": 1024, + "type": "keyword" + }, + "outif": { + "ignore_above": 1024, + "type": "keyword" + }, + "pa": { + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "ignore_above": 1024, + "type": "keyword" + }, + "per": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm": { + "ignore_above": 1024, + "type": "keyword" + }, + "perm_mask": { + "ignore_above": 1024, + "type": "keyword" + }, + "permissive": { + "ignore_above": 1024, + "type": "keyword" + }, + "pfs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pi": { + "ignore_above": 1024, + "type": "keyword" + }, + "pp": { + "ignore_above": 1024, + "type": "keyword" + }, + "printer": { + "ignore_above": 1024, + "type": "keyword" + }, + "profile": { + "ignore_above": 1024, + "type": "keyword" + }, + "prom": { + "ignore_above": 1024, + "type": "keyword" + }, + "proto": { + "ignore_above": 1024, + "type": "keyword" + }, + "qbytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "range": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" + }, + "removed": { + "ignore_above": 1024, + "type": "keyword" + }, + "res": { + "ignore_above": 1024, + "type": "keyword" + }, + "resrc": { + "ignore_above": 1024, + "type": "keyword" + }, + "rport": { + "ignore_above": 1024, + "type": "keyword" + }, + "sauid": { + "ignore_above": 1024, + "type": "keyword" + }, + "scontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "selected-context": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperm": { + "ignore_above": 1024, + "type": "keyword" + }, + "seperms": { + "ignore_above": 1024, + "type": "keyword" + }, + "seqno": { + "ignore_above": 1024, + "type": "keyword" + }, + "seresult": { + "ignore_above": 1024, + "type": "keyword" + }, + "ses": { + "ignore_above": 1024, + "type": "keyword" + }, + "seuser": { + "ignore_above": 1024, + "type": "keyword" + }, + "sig": { + "ignore_above": 1024, + "type": "keyword" + }, + "sigev_signo": { + "ignore_above": 1024, + "type": "keyword" + }, + "smac": { + "ignore_above": 1024, + "type": "keyword" + }, + "socket": { + "properties": { + "addr": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "ignore_above": 1024, + "type": "keyword" + }, + "saddr": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "spid": { + "ignore_above": 1024, + "type": "keyword" + }, + "sport": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "subj": { + "ignore_above": 1024, + "type": "keyword" + }, + "success": { + "ignore_above": 1024, + "type": "keyword" + }, + "syscall": { + "ignore_above": 1024, + "type": "keyword" + }, + "table": { + "ignore_above": 1024, + "type": "keyword" + }, + "tclass": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcontext": { + "ignore_above": 1024, + "type": "keyword" + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + }, + "tty": { + "ignore_above": 1024, + "type": "keyword" + }, + "unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "uri": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "val": { + "ignore_above": 1024, + "type": "keyword" + }, + "ver": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-ctx": { + "ignore_above": 1024, + "type": "keyword" + }, + "vm-pid": { + "ignore_above": 1024, + "type": "keyword" + }, + "watch": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "paths": { + "properties": { + "cap_fe": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fi": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fp": { + "ignore_above": 1024, + "type": "keyword" + }, + "cap_fver": { + "ignore_above": 1024, + "type": "keyword" + }, + "dev": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "item": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "nametype": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_role": { + "ignore_above": 1024, + "type": "keyword" + }, + "obj_user": { + "ignore_above": 1024, + "type": "keyword" + }, + "objtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "ogid": { + "ignore_above": 1024, + "type": "keyword" + }, + "ouid": { + "ignore_above": 1024, + "type": "keyword" + }, + "rdev": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "summary": { + "properties": { + "actor": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "how": { + "ignore_above": 1024, + "type": "keyword" + }, + "object": { + "properties": { + "primary": { + "ignore_above": 1024, + "type": "keyword" + }, + "secondary": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "origin": { + "fields": { + "raw": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "selinux": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "setgid": { + "type": "boolean" + }, + "setuid": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "flow": { + "properties": { + "complete": { + "type": "boolean" + }, + "final": { + "type": "boolean" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geoip": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "blake2b_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "blake2b_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_384": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha3_512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_224": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512_256": { + "ignore_above": 1024, + "type": "keyword" + }, + "xxh64": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "type": "object" + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "sha1": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "system": { + "properties": { + "audit": { + "properties": { + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "boottime": { + "type": "date" + }, + "containerized": { + "type": "boolean" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timezone": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "offset": { + "properties": { + "sec": { + "type": "long" + } + } + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "newsocket": { + "properties": { + "egid": { + "type": "long" + }, + "euid": { + "type": "long" + }, + "gid": { + "type": "long" + }, + "internal_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel_sock_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "type": "long" + } + } + }, + "package": { + "properties": { + "arch": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "installtime": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "release": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "summary": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socket": { + "properties": { + "egid": { + "type": "long" + }, + "euid": { + "type": "long" + }, + "gid": { + "type": "long" + }, + "internal_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel_sock_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "type": "long" + } + } + }, + "user": { + "properties": { + "dir": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "properties": { + "last_changed": { + "type": "date" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "shell": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "user_information": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "audit": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "effective": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "filesystem": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "full_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "name_map": { + "type": "object" + }, + "saved": { + "properties": { + "group": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "selinux": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "role": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "terminal": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "indexing_complete": "true", + "name": "auditbeat-8.0.0", + "rollover_alias": "auditbeat-8.0.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "query": { + "default_field": [ + "message", + "tags", + "agent.ephemeral_id", + "agent.id", + "agent.name", + "agent.type", + "agent.version", + "client.address", + "client.domain", + "client.geo.city_name", + "client.geo.continent_name", + "client.geo.country_iso_code", + "client.geo.country_name", + "client.geo.name", + "client.geo.region_iso_code", + "client.geo.region_name", + "client.mac", + "client.user.email", + "client.user.full_name", + "client.user.group.id", + "client.user.group.name", + "client.user.hash", + "client.user.id", + "client.user.name", + "cloud.account.id", + "cloud.availability_zone", + "cloud.instance.id", + "cloud.instance.name", + "cloud.machine.type", + "cloud.provider", + "cloud.region", + "container.id", + "container.image.name", + "container.image.tag", + "container.name", + "container.runtime", + "destination.address", + "destination.domain", + "destination.geo.city_name", + "destination.geo.continent_name", + "destination.geo.country_iso_code", + "destination.geo.country_name", + "destination.geo.name", + "destination.geo.region_iso_code", + "destination.geo.region_name", + "destination.mac", + "destination.user.email", + "destination.user.full_name", + "destination.user.group.id", + "destination.user.group.name", + "destination.user.hash", + "destination.user.id", + "destination.user.name", + "ecs.version", + "error.code", + "error.id", + "error.message", + "event.action", + "event.category", + "event.dataset", + "event.hash", + "event.id", + "event.kind", + "event.module", + "event.original", + "event.outcome", + "event.timezone", + "event.type", + "file.device", + "file.extension", + "file.gid", + "file.group", + "file.inode", + "file.mode", + "file.owner", + "file.path", + "file.target_path", + "file.type", + "file.uid", + "geo.city_name", + "geo.continent_name", + "geo.country_iso_code", + "geo.country_name", + "geo.name", + "geo.region_iso_code", + "geo.region_name", + "group.id", + "group.name", + "host.architecture", + "host.geo.city_name", + "host.geo.continent_name", + "host.geo.country_iso_code", + "host.geo.country_name", + "host.geo.name", + "host.geo.region_iso_code", + "host.geo.region_name", + "host.hostname", + "host.id", + "host.mac", + "host.name", + "host.os.family", + "host.os.full", + "host.os.kernel", + "host.os.name", + "host.os.platform", + "host.os.version", + "host.type", + "host.user.email", + "host.user.full_name", + "host.user.group.id", + "host.user.group.name", + "host.user.hash", + "host.user.id", + "host.user.name", + "http.request.body.content", + "http.request.method", + "http.request.referrer", + "http.response.body.content", + "http.version", + "log.level", + "log.original", + "network.application", + "network.community_id", + "network.direction", + "network.iana_number", + "network.name", + "network.protocol", + "network.transport", + "network.type", + "observer.geo.city_name", + "observer.geo.continent_name", + "observer.geo.country_iso_code", + "observer.geo.country_name", + "observer.geo.name", + "observer.geo.region_iso_code", + "observer.geo.region_name", + "observer.hostname", + "observer.mac", + "observer.os.family", + "observer.os.full", + "observer.os.kernel", + "observer.os.name", + "observer.os.platform", + "observer.os.version", + "observer.serial_number", + "observer.type", + "observer.vendor", + "observer.version", + "organization.id", + "organization.name", + "os.family", + "os.full", + "os.kernel", + "os.name", + "os.platform", + "os.version", + "process.args", + "process.executable", + "process.name", + "process.title", + "process.working_directory", + "server.address", + "server.domain", + "server.geo.city_name", + "server.geo.continent_name", + "server.geo.country_iso_code", + "server.geo.country_name", + "server.geo.name", + "server.geo.region_iso_code", + "server.geo.region_name", + "server.mac", + "server.user.email", + "server.user.full_name", + "server.user.group.id", + "server.user.group.name", + "server.user.hash", + "server.user.id", + "server.user.name", + "service.ephemeral_id", + "service.id", + "service.name", + "service.state", + "service.type", + "service.version", + "source.address", + "source.domain", + "source.geo.city_name", + "source.geo.continent_name", + "source.geo.country_iso_code", + "source.geo.country_name", + "source.geo.name", + "source.geo.region_iso_code", + "source.geo.region_name", + "source.mac", + "source.user.email", + "source.user.full_name", + "source.user.group.id", + "source.user.group.name", + "source.user.hash", + "source.user.id", + "source.user.name", + "url.domain", + "url.fragment", + "url.full", + "url.original", + "url.password", + "url.path", + "url.query", + "url.scheme", + "url.username", + "user.email", + "user.full_name", + "user.group.id", + "user.group.name", + "user.hash", + "user.id", + "user.name", + "user_agent.device.name", + "user_agent.name", + "user_agent.original", + "user_agent.os.family", + "user_agent.os.full", + "user_agent.os.kernel", + "user_agent.os.name", + "user_agent.os.platform", + "user_agent.os.version", + "user_agent.version", + "agent.hostname", + "error.type", + "cloud.project.id", + "host.os.build", + "kubernetes.pod.name", + "kubernetes.pod.uid", + "kubernetes.namespace", + "kubernetes.node.name", + "kubernetes.replicaset.name", + "kubernetes.deployment.name", + "kubernetes.statefulset.name", + "kubernetes.container.name", + "kubernetes.container.image", + "jolokia.agent.version", + "jolokia.agent.id", + "jolokia.server.product", + "jolokia.server.version", + "jolokia.server.vendor", + "jolokia.url", + "raw", + "file.origin", + "file.selinux.user", + "file.selinux.role", + "file.selinux.domain", + "file.selinux.level", + "user.audit.id", + "user.audit.name", + "user.effective.id", + "user.effective.name", + "user.effective.group.id", + "user.effective.group.name", + "user.filesystem.id", + "user.filesystem.name", + "user.filesystem.group.id", + "user.filesystem.group.name", + "user.saved.id", + "user.saved.name", + "user.saved.group.id", + "user.saved.group.name", + "user.selinux.user", + "user.selinux.role", + "user.selinux.domain", + "user.selinux.level", + "user.selinux.category", + "source.path", + "destination.path", + "auditd.message_type", + "auditd.session", + "auditd.result", + "auditd.summary.actor.primary", + "auditd.summary.actor.secondary", + "auditd.summary.object.type", + "auditd.summary.object.primary", + "auditd.summary.object.secondary", + "auditd.summary.how", + "auditd.paths.inode", + "auditd.paths.dev", + "auditd.paths.obj_user", + "auditd.paths.obj_role", + "auditd.paths.obj_domain", + "auditd.paths.obj_level", + "auditd.paths.objtype", + "auditd.paths.ouid", + "auditd.paths.rdev", + "auditd.paths.nametype", + "auditd.paths.ogid", + "auditd.paths.item", + "auditd.paths.mode", + "auditd.paths.name", + "auditd.data.action", + "auditd.data.minor", + "auditd.data.acct", + "auditd.data.addr", + "auditd.data.cipher", + "auditd.data.id", + "auditd.data.entries", + "auditd.data.kind", + "auditd.data.ksize", + "auditd.data.spid", + "auditd.data.arch", + "auditd.data.argc", + "auditd.data.major", + "auditd.data.unit", + "auditd.data.table", + "auditd.data.terminal", + "auditd.data.grantors", + "auditd.data.direction", + "auditd.data.op", + "auditd.data.tty", + "auditd.data.syscall", + "auditd.data.data", + "auditd.data.family", + "auditd.data.mac", + "auditd.data.pfs", + "auditd.data.items", + "auditd.data.a0", + "auditd.data.a1", + "auditd.data.a2", + "auditd.data.a3", + "auditd.data.hostname", + "auditd.data.lport", + "auditd.data.rport", + "auditd.data.exit", + "auditd.data.fp", + "auditd.data.laddr", + "auditd.data.sport", + "auditd.data.capability", + "auditd.data.nargs", + "auditd.data.new-enabled", + "auditd.data.audit_backlog_limit", + "auditd.data.dir", + "auditd.data.cap_pe", + "auditd.data.model", + "auditd.data.new_pp", + "auditd.data.old-enabled", + "auditd.data.oauid", + "auditd.data.old", + "auditd.data.banners", + "auditd.data.feature", + "auditd.data.vm-ctx", + "auditd.data.opid", + "auditd.data.seperms", + "auditd.data.seresult", + "auditd.data.new-rng", + "auditd.data.old-net", + "auditd.data.sigev_signo", + "auditd.data.ino", + "auditd.data.old_enforcing", + "auditd.data.old-vcpu", + "auditd.data.range", + "auditd.data.res", + "auditd.data.added", + "auditd.data.fam", + "auditd.data.nlnk-pid", + "auditd.data.subj", + "auditd.data.a[0-3]", + "auditd.data.cgroup", + "auditd.data.kernel", + "auditd.data.ocomm", + "auditd.data.new-net", + "auditd.data.permissive", + "auditd.data.class", + "auditd.data.compat", + "auditd.data.fi", + "auditd.data.changed", + "auditd.data.msg", + "auditd.data.dport", + "auditd.data.new-seuser", + "auditd.data.invalid_context", + "auditd.data.dmac", + "auditd.data.ipx-net", + "auditd.data.iuid", + "auditd.data.macproto", + "auditd.data.obj", + "auditd.data.ipid", + "auditd.data.new-fs", + "auditd.data.vm-pid", + "auditd.data.cap_pi", + "auditd.data.old-auid", + "auditd.data.oses", + "auditd.data.fd", + "auditd.data.igid", + "auditd.data.new-disk", + "auditd.data.parent", + "auditd.data.len", + "auditd.data.oflag", + "auditd.data.uuid", + "auditd.data.code", + "auditd.data.nlnk-grp", + "auditd.data.cap_fp", + "auditd.data.new-mem", + "auditd.data.seperm", + "auditd.data.enforcing", + "auditd.data.new-chardev", + "auditd.data.old-rng", + "auditd.data.outif", + "auditd.data.cmd", + "auditd.data.hook", + "auditd.data.new-level", + "auditd.data.sauid", + "auditd.data.sig", + "auditd.data.audit_backlog_wait_time", + "auditd.data.printer", + "auditd.data.old-mem", + "auditd.data.perm", + "auditd.data.old_pi", + "auditd.data.state", + "auditd.data.format", + "auditd.data.new_gid", + "auditd.data.tcontext", + "auditd.data.maj", + "auditd.data.watch", + "auditd.data.device", + "auditd.data.grp", + "auditd.data.bool", + "auditd.data.icmp_type", + "auditd.data.new_lock", + "auditd.data.old_prom", + "auditd.data.acl", + "auditd.data.ip", + "auditd.data.new_pi", + "auditd.data.default-context", + "auditd.data.inode_gid", + "auditd.data.new-log_passwd", + "auditd.data.new_pe", + "auditd.data.selected-context", + "auditd.data.cap_fver", + "auditd.data.file", + "auditd.data.net", + "auditd.data.virt", + "auditd.data.cap_pp", + "auditd.data.old-range", + "auditd.data.resrc", + "auditd.data.new-range", + "auditd.data.obj_gid", + "auditd.data.proto", + "auditd.data.old-disk", + "auditd.data.audit_failure", + "auditd.data.inif", + "auditd.data.vm", + "auditd.data.flags", + "auditd.data.nlnk-fam", + "auditd.data.old-fs", + "auditd.data.old-ses", + "auditd.data.seqno", + "auditd.data.fver", + "auditd.data.qbytes", + "auditd.data.seuser", + "auditd.data.cap_fe", + "auditd.data.new-vcpu", + "auditd.data.old-level", + "auditd.data.old_pp", + "auditd.data.daddr", + "auditd.data.old-role", + "auditd.data.ioctlcmd", + "auditd.data.smac", + "auditd.data.apparmor", + "auditd.data.fe", + "auditd.data.perm_mask", + "auditd.data.ses", + "auditd.data.cap_fi", + "auditd.data.obj_uid", + "auditd.data.reason", + "auditd.data.list", + "auditd.data.old_lock", + "auditd.data.bus", + "auditd.data.old_pe", + "auditd.data.new-role", + "auditd.data.prom", + "auditd.data.uri", + "auditd.data.audit_enabled", + "auditd.data.old-log_passwd", + "auditd.data.old-seuser", + "auditd.data.per", + "auditd.data.scontext", + "auditd.data.tclass", + "auditd.data.ver", + "auditd.data.new", + "auditd.data.val", + "auditd.data.img-ctx", + "auditd.data.old-chardev", + "auditd.data.old_val", + "auditd.data.success", + "auditd.data.inode_uid", + "auditd.data.removed", + "auditd.data.socket.port", + "auditd.data.socket.saddr", + "auditd.data.socket.addr", + "auditd.data.socket.family", + "auditd.data.socket.path", + "geoip.continent_name", + "geoip.city_name", + "geoip.region_name", + "geoip.country_iso_code", + "hash.blake2b_256", + "hash.blake2b_384", + "hash.blake2b_512", + "hash.md5", + "hash.sha1", + "hash.sha224", + "hash.sha256", + "hash.sha384", + "hash.sha3_224", + "hash.sha3_256", + "hash.sha3_384", + "hash.sha3_512", + "hash.sha512", + "hash.sha512_224", + "hash.sha512_256", + "hash.xxh64", + "event.origin", + "user.entity_id", + "user.terminal", + "process.entity_id", + "socket.entity_id", + "system.audit.host.timezone.name", + "system.audit.host.hostname", + "system.audit.host.id", + "system.audit.host.architecture", + "system.audit.host.mac", + "system.audit.host.os.platform", + "system.audit.host.os.name", + "system.audit.host.os.family", + "system.audit.host.os.version", + "system.audit.host.os.kernel", + "system.audit.package.entity_id", + "system.audit.package.name", + "system.audit.package.version", + "system.audit.package.release", + "system.audit.package.arch", + "system.audit.package.license", + "system.audit.package.summary", + "system.audit.package.url", + "system.audit.user.name", + "system.audit.user.uid", + "system.audit.user.gid", + "system.audit.user.dir", + "system.audit.user.shell", + "system.audit.user.user_information", + "system.audit.user.password.type", + "fields.*" + ] + }, + "refresh_interval": "5s" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json new file mode 100644 index 0000000000000..dfe0444e0bbd4 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -0,0 +1,13 @@ +{ + "type": "doc", + "value": { + "id": "_uZE6nwBOpWiDweSth_D", + "index": "threat-indicator-0001", + "source": { + "@timestamp": "2019-09-01T00:41:06.527Z", + "agent": { + "threat": "03ccb0ce-f65c-4279-a619-05f1d5bb000b" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json new file mode 100644 index 0000000000000..0c24fa429d908 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -0,0 +1,30 @@ +{ + "type": "index", + "value": { + "aliases": { + "threat-indicator": { + "is_write_index": false + } + }, + "index": "threat-indicator-0001", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } +} From bb7698958252d60724d542edaa7cf54e47cfbc21 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky <streamich@gmail.com> Date: Thu, 3 Dec 2020 11:51:56 +0100 Subject: [PATCH 093/107] =?UTF-8?q?fix:=20=F0=9F=90=9B=20don't=20add=20sep?= =?UTF-8?q?arator=20befor=20group=20on=20no=20main=20items=20(#83166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../build_eui_context_menu_panels.test.ts | 202 +++++++++++++++++- .../build_eui_context_menu_panels.tsx | 10 +- 2 files changed, 207 insertions(+), 5 deletions(-) diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts index 3a598b547e343..3111a0b55084c 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts @@ -20,25 +20,31 @@ import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { buildContextMenuForActions } from './build_eui_context_menu_panels'; import { Action, createAction } from '../actions'; +import { PresentableGrouping } from '../util'; const createTestAction = ({ type, dispayName, order, + grouping = undefined, }: { type?: string; dispayName: string; order?: number; + grouping?: PresentableGrouping; }) => createAction({ type: type as any, // mapping doesn't matter for this test getDisplayName: () => dispayName, order, execute: async () => {}, + grouping, }); const resultMapper = (panel: EuiContextMenuPanelDescriptor) => ({ - items: panel.items ? panel.items.map((item) => ({ name: item.name })) : [], + items: panel.items + ? panel.items.map((item) => ({ name: item.isSeparator ? 'SEPARATOR' : item.name })) + : [], }); test('sorts items in DESC order by "order" field first, then by display name', async () => { @@ -237,3 +243,197 @@ test('hides items behind in "More" submenu if there are more than 4 actions', as ] `); }); + +test('separates grouped items from main items with a separator', async () => { + const actions = [ + createTestAction({ + dispayName: 'Foo 1', + }), + createTestAction({ + dispayName: 'Foo 2', + }), + createTestAction({ + dispayName: 'Foo 3', + }), + createTestAction({ + dispayName: 'Foo 4', + grouping: [ + { + id: 'testGroup', + getDisplayName: () => 'Test group', + }, + ], + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "Foo 1", + }, + Object { + "name": "Foo 2", + }, + Object { + "name": "Foo 3", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + ], + }, + ] + `); +}); + +test('separates multiple groups each with its own separator', async () => { + const actions = [ + createTestAction({ + dispayName: 'Foo 1', + }), + createTestAction({ + dispayName: 'Foo 2', + }), + createTestAction({ + dispayName: 'Foo 3', + }), + createTestAction({ + dispayName: 'Foo 4', + grouping: [ + { + id: 'testGroup', + getDisplayName: () => 'Test group', + }, + ], + }), + createTestAction({ + dispayName: 'Foo 5', + grouping: [ + { + id: 'testGroup2', + getDisplayName: () => 'Test group 2', + }, + ], + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "Foo 1", + }, + Object { + "name": "Foo 2", + }, + Object { + "name": "Foo 3", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 4", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 5", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 5", + }, + ], + }, + ] + `); +}); + +test('does not add separator for first grouping if there are no main items', async () => { + const actions = [ + createTestAction({ + dispayName: 'Foo 4', + grouping: [ + { + id: 'testGroup', + getDisplayName: () => 'Test group', + }, + ], + }), + createTestAction({ + dispayName: 'Foo 5', + grouping: [ + { + id: 'testGroup2', + getDisplayName: () => 'Test group 2', + }, + ], + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + Object { + "name": "SEPARATOR", + }, + Object { + "name": "Foo 5", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 5", + }, + ], + }, + ] + `); +}); diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index c7efb6dad326d..63586ca3da1f7 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -201,10 +201,12 @@ export async function buildContextMenuForActions({ for (const panel of Object.values(panels)) { if (panel._level === 0) { - panels.mainMenu.items.push({ - isSeparator: true, - key: panel.id + '__separator', - }); + if (panels.mainMenu.items.length > 0) { + panels.mainMenu.items.push({ + isSeparator: true, + key: panel.id + '__separator', + }); + } if (panel.items.length > 3) { panels.mainMenu.items.push({ name: panel.title || panel.id, From 3cb26ebe8d8aaabcf16daf18fe608ab591d2ab77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= <weltenwort@users.noreply.github.com> Date: Thu, 3 Dec 2020 13:05:15 +0100 Subject: [PATCH 094/107] [Logs UI] Fetch single log entries via a search strategy (#81710) This replaces the log item API with a single-log-entry search strategy. This is used to fetch the data for display in the details flyout. There should be no significant visual difference to the user. --- .../common/http_api/log_entries/entries.ts | 14 +- .../common/http_api/log_entries/highlights.ts | 6 +- .../common/http_api/log_entries/index.ts | 2 - .../infra/common/http_api/log_entries/item.ts | 32 --- .../log_entries/summary_highlights.ts | 4 +- .../plugins/infra/common/log_entry/index.ts | 1 + .../common/log_entry/log_entry_cursor.ts | 21 ++ x-pack/plugins/infra/common/runtime_types.ts | 35 ++- .../common/search_strategies/common/errors.ts | 26 ++ .../log_entries/log_entry.ts | 46 ++++ .../public/components/log_stream/index.tsx | 4 +- .../log_entry_actions_menu.test.tsx | 16 +- .../log_entry_actions_menu.tsx | 47 ++-- .../log_entry_flyout/log_entry_flyout.tsx | 81 +++++-- .../log_entries/api/fetch_log_entries_item.ts | 28 --- .../logs/log_entries/api/fetch_log_entry.ts | 31 +++ .../public/containers/logs/log_flyout.tsx | 20 +- .../containers/logs/log_stream/index.ts | 9 +- .../log_entry_rate/page_results_content.tsx | 11 +- .../pages/logs/stream/page_logs_content.tsx | 2 + x-pack/plugins/infra/server/infra_server.ts | 2 - .../lib/adapters/framework/adapter_types.ts | 11 +- .../framework/kibana_framework_adapter.ts | 6 +- .../log_entries/kibana_log_entries_adapter.ts | 38 +-- ...document_source_to_log_item_fields.test.ts | 66 ----- ...vert_document_source_to_log_item_fields.ts | 25 -- .../log_entries_domain/log_entries_domain.ts | 54 +---- .../plugins/infra/server/lib/sources/mocks.ts | 19 ++ .../infra/server/lib/sources/sources.ts | 3 + x-pack/plugins/infra/server/plugin.ts | 29 ++- .../infra/server/routes/log_entries/index.ts | 1 - .../infra/server/routes/log_entries/item.ts | 43 ---- .../services/log_entries/index.ts} | 8 +- .../log_entries/log_entries_service.ts | 23 ++ .../log_entry_search_strategy.test.ts | 225 ++++++++++++++++++ .../log_entries/log_entry_search_strategy.ts | 124 ++++++++++ .../services/log_entries/queries/log_entry.ts | 57 +++++ .../server/services/log_entries/types.ts | 20 ++ .../utils/elasticsearch_runtime_types.ts | 34 ++- .../server/utils/typed_search_strategy.ts | 57 +++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../api_integration/apis/metrics_ui/index.js | 1 - .../apis/metrics_ui/log_item.ts | 152 ------------ 44 files changed, 880 insertions(+), 556 deletions(-) delete mode 100644 x-pack/plugins/infra/common/http_api/log_entries/item.ts create mode 100644 x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts create mode 100644 x-pack/plugins/infra/common/search_strategies/common/errors.ts create mode 100644 x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts delete mode 100644 x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts delete mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts delete mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts create mode 100644 x-pack/plugins/infra/server/lib/sources/mocks.ts delete mode 100644 x-pack/plugins/infra/server/routes/log_entries/item.ts rename x-pack/plugins/infra/{common/http_api/log_entries/common.ts => server/services/log_entries/index.ts} (55%) create mode 100644 x-pack/plugins/infra/server/services/log_entries/log_entries_service.ts create mode 100644 x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts create mode 100644 x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts create mode 100644 x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts create mode 100644 x-pack/plugins/infra/server/services/log_entries/types.ts create mode 100644 x-pack/plugins/infra/server/utils/typed_search_strategy.ts delete mode 100644 x-pack/test/api_integration/apis/metrics_ui/log_item.ts diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index 5f35eb89774fa..31bc62f48791a 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -5,8 +5,8 @@ */ import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; import { jsonArrayRT } from '../../typed_json'; -import { logEntriesCursorRT } from './common'; import { logSourceColumnConfigurationRT } from '../log_sources'; export const LOG_ENTRIES_PATH = '/api/log_entries/entries'; @@ -26,17 +26,17 @@ export const logEntriesBaseRequestRT = rt.intersection([ export const logEntriesBeforeRequestRT = rt.intersection([ logEntriesBaseRequestRT, - rt.type({ before: rt.union([logEntriesCursorRT, rt.literal('last')]) }), + rt.type({ before: rt.union([logEntryCursorRT, rt.literal('last')]) }), ]); export const logEntriesAfterRequestRT = rt.intersection([ logEntriesBaseRequestRT, - rt.type({ after: rt.union([logEntriesCursorRT, rt.literal('first')]) }), + rt.type({ after: rt.union([logEntryCursorRT, rt.literal('first')]) }), ]); export const logEntriesCenteredRequestRT = rt.intersection([ logEntriesBaseRequestRT, - rt.type({ center: logEntriesCursorRT }), + rt.type({ center: logEntryCursorRT }), ]); export const logEntriesRequestRT = rt.union([ @@ -85,7 +85,7 @@ export const logEntryContextRT = rt.union([ export const logEntryRT = rt.type({ id: rt.string, - cursor: logEntriesCursorRT, + cursor: logEntryCursorRT, columns: rt.array(logColumnRT), context: logEntryContextRT, }); @@ -104,8 +104,8 @@ export const logEntriesResponseRT = rt.type({ data: rt.intersection([ rt.type({ entries: rt.array(logEntryRT), - topCursor: rt.union([logEntriesCursorRT, rt.null]), - bottomCursor: rt.union([logEntriesCursorRT, rt.null]), + topCursor: rt.union([logEntryCursorRT, rt.null]), + bottomCursor: rt.union([logEntryCursorRT, rt.null]), }), rt.partial({ hasMoreBefore: rt.boolean, diff --git a/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts b/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts index 811cf85db8883..648da43134a27 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts @@ -5,6 +5,7 @@ */ import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; import { logEntriesBaseRequestRT, logEntriesBeforeRequestRT, @@ -12,7 +13,6 @@ import { logEntriesCenteredRequestRT, logEntryRT, } from './entries'; -import { logEntriesCursorRT } from './common'; export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights'; @@ -58,8 +58,8 @@ export const logEntriesHighlightsResponseRT = rt.type({ entries: rt.array(logEntryRT), }), rt.type({ - topCursor: logEntriesCursorRT, - bottomCursor: logEntriesCursorRT, + topCursor: logEntryCursorRT, + bottomCursor: logEntryCursorRT, entries: rt.array(logEntryRT), }), ]) diff --git a/x-pack/plugins/infra/common/http_api/log_entries/index.ts b/x-pack/plugins/infra/common/http_api/log_entries/index.ts index 490f295cbff68..9e34c1fc91199 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/index.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './common'; export * from './entries'; export * from './highlights'; -export * from './item'; export * from './summary'; export * from './summary_highlights'; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/item.ts b/x-pack/plugins/infra/common/http_api/log_entries/item.ts deleted file mode 100644 index 5f9457b8228ac..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_entries/item.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as rt from 'io-ts'; -import { logEntriesCursorRT } from './common'; - -export const LOG_ENTRIES_ITEM_PATH = '/api/log_entries/item'; - -export const logEntriesItemRequestRT = rt.type({ - sourceId: rt.string, - id: rt.string, -}); - -export type LogEntriesItemRequest = rt.TypeOf<typeof logEntriesItemRequestRT>; - -const logEntriesItemFieldRT = rt.type({ field: rt.string, value: rt.array(rt.string) }); -const logEntriesItemRT = rt.type({ - id: rt.string, - index: rt.string, - fields: rt.array(logEntriesItemFieldRT), - key: logEntriesCursorRT, -}); -export const logEntriesItemResponseRT = rt.type({ - data: logEntriesItemRT, -}); - -export type LogEntriesItemField = rt.TypeOf<typeof logEntriesItemFieldRT>; -export type LogEntriesItem = rt.TypeOf<typeof logEntriesItemRT>; -export type LogEntriesItemResponse = rt.TypeOf<typeof logEntriesItemResponseRT>; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts b/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts index 30222cd71bbde..7da1e7bc71c79 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts @@ -5,8 +5,8 @@ */ import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; import { logEntriesSummaryRequestRT, logEntriesSummaryBucketRT } from './summary'; -import { logEntriesCursorRT } from './common'; export const LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH = '/api/log_entries/summary_highlights'; @@ -24,7 +24,7 @@ export type LogEntriesSummaryHighlightsRequest = rt.TypeOf< export const logEntriesSummaryHighlightsBucketRT = rt.intersection([ logEntriesSummaryBucketRT, rt.type({ - representativeKey: logEntriesCursorRT, + representativeKey: logEntryCursorRT, }), ]); diff --git a/x-pack/plugins/infra/common/log_entry/index.ts b/x-pack/plugins/infra/common/log_entry/index.ts index 66cc5108b6692..0654735499fab 100644 --- a/x-pack/plugins/infra/common/log_entry/index.ts +++ b/x-pack/plugins/infra/common/log_entry/index.ts @@ -5,3 +5,4 @@ */ export * from './log_entry'; +export * from './log_entry_cursor'; diff --git a/x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts b/x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts new file mode 100644 index 0000000000000..280403dd5438d --- /dev/null +++ b/x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { decodeOrThrow } from '../runtime_types'; + +export const logEntryCursorRT = rt.type({ + time: rt.number, + tiebreaker: rt.number, +}); + +export type LogEntryCursor = rt.TypeOf<typeof logEntryCursorRT>; + +export const getLogEntryCursorFromHit = (hit: { sort: [number, number] }) => + decodeOrThrow(logEntryCursorRT)({ + time: hit.sort[0], + tiebreaker: hit.sort[1], + }); diff --git a/x-pack/plugins/infra/common/runtime_types.ts b/x-pack/plugins/infra/common/runtime_types.ts index a8d5cd8693a3d..a26121a5dd225 100644 --- a/x-pack/plugins/infra/common/runtime_types.ts +++ b/x-pack/plugins/infra/common/runtime_types.ts @@ -7,16 +7,41 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { Errors, Type } from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; -import { RouteValidationFunction } from 'kibana/server'; +import { Context, Errors, IntersectionType, Type, UnionType, ValidationError } from 'io-ts'; +import type { RouteValidationFunction } from 'kibana/server'; type ErrorFactory = (message: string) => Error; +const getErrorPath = ([first, ...rest]: Context): string[] => { + if (typeof first === 'undefined') { + return []; + } else if (first.type instanceof IntersectionType) { + const [, ...next] = rest; + return getErrorPath(next); + } else if (first.type instanceof UnionType) { + const [, ...next] = rest; + return [first.key, ...getErrorPath(next)]; + } + + return [first.key, ...getErrorPath(rest)]; +}; + +const getErrorType = ({ context }: ValidationError) => + context[context.length - 1]?.type?.name ?? 'unknown'; + +const formatError = (error: ValidationError) => + error.message ?? + `in ${getErrorPath(error.context).join('/')}: ${JSON.stringify( + error.value + )} does not match expected type ${getErrorType(error)}`; + +const formatErrors = (errors: ValidationError[]) => + `Failed to validate: \n${errors.map((error) => ` ${formatError(error)}`).join('\n')}`; + export const createPlainError = (message: string) => new Error(message); export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => { - throw createError(failure(errors).join('\n')); + throw createError(formatErrors(errors)); }; export const decodeOrThrow = <DecodedValue, EncodedValue, InputValue>( @@ -33,7 +58,7 @@ export const createValidationFunction = <DecodedValue, EncodedValue, InputValue> pipe( runtimeType.decode(inputValue), fold<Errors, DecodedValue, ValdidationResult<DecodedValue>>( - (errors: Errors) => badRequest(failure(errors).join('\n')), + (errors: Errors) => badRequest(formatErrors(errors)), (result: DecodedValue) => ok(result) ) ); diff --git a/x-pack/plugins/infra/common/search_strategies/common/errors.ts b/x-pack/plugins/infra/common/search_strategies/common/errors.ts new file mode 100644 index 0000000000000..4f7954c09c48b --- /dev/null +++ b/x-pack/plugins/infra/common/search_strategies/common/errors.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +const genericErrorRT = rt.type({ + type: rt.literal('generic'), + message: rt.string, +}); + +const shardFailureErrorRT = rt.type({ + type: rt.literal('shardFailure'), + shardInfo: rt.type({ + shard: rt.number, + index: rt.string, + node: rt.string, + }), + message: rt.string, +}); + +export const searchStrategyErrorRT = rt.union([genericErrorRT, shardFailureErrorRT]); + +export type SearchStrategyError = rt.TypeOf<typeof searchStrategyErrorRT>; diff --git a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts new file mode 100644 index 0000000000000..af6bd203f980e --- /dev/null +++ b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; +import { jsonArrayRT } from '../../typed_json'; +import { searchStrategyErrorRT } from '../common/errors'; + +export const LOG_ENTRY_SEARCH_STRATEGY = 'infra-log-entry'; + +export const logEntrySearchRequestParamsRT = rt.type({ + sourceId: rt.string, + logEntryId: rt.string, +}); + +export type LogEntrySearchRequestParams = rt.TypeOf<typeof logEntrySearchRequestParamsRT>; + +const logEntryFieldRT = rt.type({ + field: rt.string, + value: jsonArrayRT, +}); + +export type LogEntryField = rt.TypeOf<typeof logEntryFieldRT>; + +export const logEntryRT = rt.type({ + id: rt.string, + index: rt.string, + fields: rt.array(logEntryFieldRT), + key: logEntryCursorRT, +}); + +export type LogEntry = rt.TypeOf<typeof logEntryRT>; + +export const logEntrySearchResponsePayloadRT = rt.intersection([ + rt.type({ + data: rt.union([logEntryRT, rt.null]), + }), + rt.partial({ + errors: rt.array(searchStrategyErrorRT), + }), +]); + +export type LogEntrySearchResponsePayload = rt.TypeOf<typeof logEntrySearchResponsePayloadRT>; diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index c4e6bbe094642..3ca1ed7d4726f 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; import { euiStyled } from '../../../../observability/public'; -import { LogEntriesCursor } from '../../../common/http_api'; +import { LogEntryCursor } from '../../../common/log_entry'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { LogSourceConfigurationProperties, useLogSource } from '../../containers/logs/log_source'; @@ -28,7 +28,7 @@ export interface LogStreamProps { startTimestamp: number; endTimestamp: number; query?: string; - center?: LogEntriesCursor; + center?: LogEntryCursor; highlight?: string; height?: string | number; columns?: LogColumnDefinition[]; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx index 77154474077c8..f578292d6d6fc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx @@ -28,7 +28,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [{ field: 'host.ip', value: ['HOST_IP'] }], id: 'ITEM_ID', index: 'INDEX', @@ -58,7 +58,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [{ field: 'container.id', value: ['CONTAINER_ID'] }], id: 'ITEM_ID', index: 'INDEX', @@ -88,7 +88,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [{ field: 'kubernetes.pod.uid', value: ['POD_UID'] }], id: 'ITEM_ID', index: 'INDEX', @@ -118,7 +118,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [ { field: 'container.id', value: ['CONTAINER_ID'] }, { field: 'host.ip', value: ['HOST_IP'] }, @@ -154,7 +154,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [], id: 'ITEM_ID', index: 'INDEX', @@ -188,7 +188,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [{ field: 'trace.id', value: ['1234567'] }], id: 'ITEM_ID', index: 'INDEX', @@ -219,7 +219,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [ { field: 'trace.id', value: ['1234567'] }, { field: '@timestamp', value: [timestamp] }, @@ -252,7 +252,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( <ProviderWrapper> <LogEntryActionsMenu - logItem={{ + logEntry={{ fields: [], id: 'ITEM_ID', index: 'INDEX', 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 e1ecb32afb4be..aa3b4532e878e 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 @@ -9,18 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import { useVisibilityState } from '../../../utils/use_visibility_state'; import { getTraceUrl } from '../../../../../apm/public'; -import { LogEntriesItem } from '../../../../common/http_api'; import { useLinkProps, LinkDescriptor } from '../../../hooks/use_link_props'; +import { LogEntry } from '../../../../common/search_strategies/log_entries/log_entry'; const UPTIME_FIELDS = ['container.id', 'host.ip', 'kubernetes.pod.uid']; export const LogEntryActionsMenu: React.FunctionComponent<{ - logItem: LogEntriesItem; -}> = ({ logItem }) => { + logEntry: LogEntry; +}> = ({ logEntry }) => { const { hide, isVisible, show } = useVisibilityState(false); - const apmLinkDescriptor = useMemo(() => getAPMLink(logItem), [logItem]); - const uptimeLinkDescriptor = useMemo(() => getUptimeLink(logItem), [logItem]); + const apmLinkDescriptor = useMemo(() => getAPMLink(logEntry), [logEntry]); + const uptimeLinkDescriptor = useMemo(() => getUptimeLink(logEntry), [logEntry]); const uptimeLinkProps = useLinkProps({ app: 'uptime', @@ -90,8 +90,8 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ ); }; -const getUptimeLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { - const searchExpressions = logItem.fields +const getUptimeLink = (logEntry: LogEntry): LinkDescriptor | undefined => { + const searchExpressions = logEntry.fields .filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field)) .reduce<string[]>((acc, fieldItem) => { const { field, value } = fieldItem; @@ -110,31 +110,32 @@ const getUptimeLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { }; }; -const getAPMLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { - const traceIdEntry = logItem.fields.find( - ({ field, value }) => value[0] != null && field === 'trace.id' - ); +const getAPMLink = (logEntry: LogEntry): LinkDescriptor | undefined => { + const traceId = logEntry.fields.find( + ({ field, value }) => typeof value[0] === 'string' && field === 'trace.id' + )?.value?.[0]; - if (!traceIdEntry) { + if (typeof traceId !== 'string') { return undefined; } - const timestampField = logItem.fields.find(({ field }) => field === '@timestamp'); + const timestampField = logEntry.fields.find(({ field }) => field === '@timestamp'); const timestamp = timestampField ? timestampField.value[0] : null; - const { rangeFrom, rangeTo } = timestamp - ? (() => { - const from = new Date(timestamp); - const to = new Date(timestamp); + const { rangeFrom, rangeTo } = + typeof timestamp === 'number' + ? (() => { + const from = new Date(timestamp); + const to = new Date(timestamp); - from.setMinutes(from.getMinutes() - 10); - to.setMinutes(to.getMinutes() + 10); + from.setMinutes(from.getMinutes() - 10); + to.setMinutes(to.getMinutes() + 10); - return { rangeFrom: from.toISOString(), rangeTo: to.toISOString() }; - })() - : { rangeFrom: 'now-1y', rangeTo: 'now' }; + return { rangeFrom: from.toISOString(), rangeTo: to.toISOString() }; + })() + : { rangeFrom: 'now-1y', rangeTo: 'now' }; return { app: 'apm', - hash: getTraceUrl({ traceId: traceIdEntry.value[0], rangeFrom, rangeTo }), + hash: getTraceUrl({ traceId, rangeFrom, rangeTo }), }; }; 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 b07d8c9dce23c..bc0f6dc97017a 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 @@ -5,13 +5,16 @@ */ import { - EuiBasicTable, + EuiBasicTableColumn, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiInMemoryTable, + EuiSpacer, + EuiTextColor, EuiTitle, EuiToolTip, } from '@elastic/eui'; @@ -19,28 +22,49 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import React, { useCallback, useMemo } from 'react'; - import { euiStyled } from '../../../../../observability/public'; +import { + LogEntry, + LogEntryField, +} from '../../../../common/search_strategies/log_entries/log_entry'; import { TimeKey } from '../../../../common/time'; import { InfraLoadingPanel } from '../../loading'; +import { FieldValue } from '../log_text_stream/field_value'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; -import { LogEntriesItem, LogEntriesItemField } from '../../../../common/http_api'; export interface LogEntryFlyoutProps { - flyoutItem: LogEntriesItem | null; + flyoutError: string | null; + flyoutItem: LogEntry | null; setFlyoutVisibility: (visible: boolean) => void; setFilter: (filter: string, flyoutItemId: string, timeKey?: TimeKey) => void; loading: boolean; } +const emptyHighlightTerms: string[] = []; + +const initialSortingOptions = { + sort: { + field: 'field', + direction: 'asc' as const, + }, +}; + +const searchOptions = { + box: { + incremental: true, + schema: true, + }, +}; + export const LogEntryFlyout = ({ + flyoutError, flyoutItem, loading, setFlyoutVisibility, setFilter, }: LogEntryFlyoutProps) => { const createFilterHandler = useCallback( - (field: LogEntriesItemField) => () => { + (field: LogEntryField) => () => { if (!flyoutItem) { return; } @@ -63,7 +87,7 @@ export const LogEntryFlyout = ({ const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]); - const columns = useMemo( + const columns = useMemo<Array<EuiBasicTableColumn<LogEntryField>>>( () => [ { field: 'field', @@ -77,8 +101,7 @@ export const LogEntryFlyout = ({ name: i18n.translate('xpack.infra.logFlyout.valueColumnLabel', { defaultMessage: 'Value', }), - sortable: true, - render: (_name: string, item: LogEntriesItemField) => ( + render: (_name: string, item: LogEntryField) => ( <span> <EuiToolTip content={i18n.translate('xpack.infra.logFlyout.setFilterTooltip', { @@ -94,7 +117,11 @@ export const LogEntryFlyout = ({ onClick={createFilterHandler(item)} /> </EuiToolTip> - {formatValue(item.value)} + <FieldValue + highlightTerms={emptyHighlightTerms} + isActiveHighlight={false} + value={item.value} + /> </span> ), }, @@ -110,19 +137,36 @@ export const LogEntryFlyout = ({ <EuiTitle size="s"> <h3 id="flyoutTitle"> <FormattedMessage - defaultMessage="Log event document details" + defaultMessage="Details for log entry {logEntryId}" id="xpack.infra.logFlyout.flyoutTitle" + values={{ + logEntryId: flyoutItem ? <code>{flyoutItem.id}</code> : '', + }} /> </h3> </EuiTitle> + {flyoutItem ? ( + <> + <EuiSpacer size="s" /> + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.infra.logFlyout.flyoutSubTitle" + defaultMessage="From index {indexName}" + values={{ + indexName: <code>{flyoutItem.index}</code>, + }} + /> + </EuiTextColor> + </> + ) : null} </EuiFlexItem> <EuiFlexItem grow={false}> - {flyoutItem !== null ? <LogEntryActionsMenu logItem={flyoutItem} /> : null} + {flyoutItem !== null ? <LogEntryActionsMenu logEntry={flyoutItem} /> : null} </EuiFlexItem> </EuiFlexGroup> </EuiFlyoutHeader> <EuiFlyoutBody> - {loading || flyoutItem === null ? ( + {loading ? ( <InfraFlyoutLoadingPanel> <InfraLoadingPanel height="100%" @@ -132,8 +176,15 @@ export const LogEntryFlyout = ({ })} /> </InfraFlyoutLoadingPanel> + ) : flyoutItem ? ( + <EuiInMemoryTable<LogEntryField> + columns={columns} + items={flyoutItem.fields} + search={searchOptions} + sorting={initialSortingOptions} + /> ) : ( - <EuiBasicTable columns={columns} items={flyoutItem.fields} /> + <InfraFlyoutLoadingPanel>{flyoutError}</InfraFlyoutLoadingPanel> )} </EuiFlyoutBody> </EuiFlyout> @@ -147,7 +198,3 @@ export const InfraFlyoutLoadingPanel = euiStyled.div` bottom: 0; left: 0; `; - -function formatValue(value: string[]) { - return value.length > 1 ? value.join(', ') : value[0]; -} diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts deleted file mode 100644 index d459fba6cf957..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import type { HttpHandler } from 'src/core/public'; - -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -import { - LOG_ENTRIES_ITEM_PATH, - LogEntriesItemRequest, - logEntriesItemRequestRT, - logEntriesItemResponseRT, -} from '../../../../../common/http_api'; - -export const fetchLogEntriesItem = async ( - requestArgs: LogEntriesItemRequest, - fetch: HttpHandler -) => { - const response = await fetch(LOG_ENTRIES_ITEM_PATH, { - method: 'POST', - body: JSON.stringify(logEntriesItemRequestRT.encode(requestArgs)), - }); - - return decodeOrThrow(logEntriesItemResponseRT)(response); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts new file mode 100644 index 0000000000000..764de1d34a3bf --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ISearchStart } from '../../../../../../../../src/plugins/data/public'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { + LogEntry, + LogEntrySearchRequestParams, + logEntrySearchRequestParamsRT, + logEntrySearchResponsePayloadRT, + LOG_ENTRY_SEARCH_STRATEGY, +} from '../../../../../common/search_strategies/log_entries/log_entry'; + +export { LogEntry }; + +export const fetchLogEntry = async ( + requestArgs: LogEntrySearchRequestParams, + search: ISearchStart +) => { + const response = await search + .search( + { params: logEntrySearchRequestParamsRT.encode(requestArgs) }, + { strategy: LOG_ENTRY_SEARCH_STRATEGY } + ) + .toPromise(); + + return decodeOrThrow(logEntrySearchResponsePayloadRT)(response.rawResponse); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 9ed2f5ad175c7..121f0e6b651dc 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx @@ -7,12 +7,10 @@ import createContainer from 'constate'; import { isString } from 'lodash'; import React, { useContext, useEffect, useMemo, useState } from 'react'; - -import { LogEntriesItem } from '../../../common/http_api'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { UrlStateContainer } from '../../utils/url_state'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; -import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item'; +import { fetchLogEntry } from './log_entries/api/fetch_log_entry'; import { useLogSourceContext } from './log_source'; export enum FlyoutVisibility { @@ -31,7 +29,6 @@ export const useLogFlyout = () => { const { sourceId } = useLogSourceContext(); const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false); const [flyoutId, setFlyoutId] = useState<string | null>(null); - const [flyoutItem, setFlyoutItem] = useState<LogEntriesItem | null>(null); const [surroundingLogsId, setSurroundingLogsId] = useState<string | null>(null); const [loadFlyoutItemRequest, loadFlyoutItem] = useTrackedPromise( @@ -39,15 +36,9 @@ export const useLogFlyout = () => { cancelPreviousOn: 'creation', createPromise: async () => { if (!flyoutId) { - return; - } - return await fetchLogEntriesItem({ sourceId, id: flyoutId }, services.http.fetch); - }, - onResolve: (response) => { - if (response) { - const { data } = response; - setFlyoutItem(data || null); + throw new Error('Failed to load log entry: Id not specified.'); } + return await fetchLogEntry({ sourceId, logEntryId: flyoutId }, services.data.search); }, }, [sourceId, flyoutId] @@ -71,7 +62,10 @@ export const useLogFlyout = () => { surroundingLogsId, setSurroundingLogsId, isLoading, - flyoutItem, + flyoutItem: + loadFlyoutItemRequest.state === 'resolved' ? loadFlyoutItemRequest.value.data : null, + flyoutError: + loadFlyoutItemRequest.state === 'rejected' ? `${loadFlyoutItemRequest.value}` : null, }; }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index b0b09c76f4d85..ff30e993aa3a9 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -10,7 +10,8 @@ import usePrevious from 'react-use/lib/usePrevious'; import { esKuery } from '../../../../../../../src/plugins/data/public'; import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { LogEntry, LogEntriesCursor } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/http_api'; +import { LogEntryCursor } from '../../../../common/log_entry'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { LogSourceConfigurationProperties } from '../log_source'; @@ -19,14 +20,14 @@ interface LogStreamProps { startTimestamp: number; endTimestamp: number; query?: string; - center?: LogEntriesCursor; + center?: LogEntryCursor; columns?: LogSourceConfigurationProperties['logColumns']; } interface LogStreamState { entries: LogEntry[]; - topCursor: LogEntriesCursor | null; - bottomCursor: LogEntriesCursor | null; + topCursor: LogEntryCursor | null; + bottomCursor: LogEntryCursor | null; hasMoreBefore: boolean; hasMoreAfter: boolean; } diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index b33eaf7e77bc3..bb0c9196fb0cc 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -144,9 +144,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { filteredDatasets: selectedDatasets, }); - const { flyoutVisible, setFlyoutVisibility, flyoutItem, isLoading: isFlyoutLoading } = useContext( - LogFlyout.Context - ); + const { + flyoutVisible, + setFlyoutVisibility, + flyoutError, + flyoutItem, + isLoading: isFlyoutLoading, + } = useContext(LogFlyout.Context); const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { @@ -304,6 +308,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { {flyoutVisible ? ( <LogEntryFlyout + flyoutError={flyoutError} flyoutItem={flyoutItem} setFlyoutVisibility={setFlyoutVisibility} loading={isFlyoutLoading} diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index ea9494c7073d4..aa78ec9f515bf 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -37,6 +37,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { surroundingLogsId, setSurroundingLogsId, flyoutItem, + flyoutError, isLoading, } = useContext(LogFlyoutState.Context); const { logSummaryHighlights } = useContext(LogHighlightsState.Context); @@ -80,6 +81,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { setFilter={setFilter} setFlyoutVisibility={setFlyoutVisibility} flyoutItem={flyoutItem} + flyoutError={flyoutError} loading={isLoading} /> ) : null} diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 2bf5687da7e08..6c0d4e9d302ee 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -34,7 +34,6 @@ import { initLogEntriesHighlightsRoute, initLogEntriesSummaryRoute, initLogEntriesSummaryHighlightsRoute, - initLogEntriesItemRoute, } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; @@ -74,7 +73,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogEntriesHighlightsRoute(libs); initLogEntriesSummaryRoute(libs); initLogEntriesSummaryHighlightsRoute(libs); - initLogEntriesItemRoute(libs); initMetricExplorerRoute(libs); initMetricsAPIRoute(libs); initMetadataRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index ad82939ec7f9d..93a7bc9a0830b 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,10 @@ import { GenericParams, SearchResponse } from 'elasticsearch'; import { Lifecycle } from '@hapi/hapi'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { RouteConfig, RouteMethod } from '../../../../../../../src/core/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../../../../src/plugins/data/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginSetup } from '../../../../../../plugins/apm/server'; @@ -17,7 +21,8 @@ import { PluginSetupContract as AlertingPluginContract } from '../../../../../al import { MlPluginSetup } from '../../../../../ml/server'; import { JsonArray, JsonValue } from '../../../../common/typed_json'; -export interface InfraServerPluginDeps { +export interface InfraServerPluginSetupDeps { + data: DataPluginSetup; home: HomeServerPluginSetup; spaces: SpacesPluginSetup; usageCollection: UsageCollectionSetup; @@ -28,6 +33,10 @@ export interface InfraServerPluginDeps { ml?: MlPluginSetup; } +export interface InfraServerPluginStartDeps { + data: DataPluginStart; +} + export interface CallWithRequestParams extends GenericParams { max_concurrent_shard_requests?: number; name?: string; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 2d84e36f3a3ac..7f686b4d7717c 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -10,7 +10,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { InfraRouteConfig, InfraTSVBResponse, - InfraServerPluginDeps, + InfraServerPluginSetupDeps, CallWithRequestParams, InfraDatabaseSearchResponse, InfraDatabaseMultiResponse, @@ -33,9 +33,9 @@ import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plug export class KibanaFramework { public router: IRouter; - public plugins: InfraServerPluginDeps; + public plugins: InfraServerPluginSetupDeps; - constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginSetupDeps) { this.router = core.http.createRouter(); this.plugins = plugins; } diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 6ffa1ad4b0b82..4637f3ab41782 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -9,12 +9,11 @@ import { fold, map } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as runtimeTypes from 'io-ts'; -import { compact, first } from 'lodash'; +import { compact } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { JsonArray } from '../../../../common/typed_json'; import { LogEntriesAdapter, - LogItemHit, LogEntriesParams, LogEntryDocument, LogEntryQuery, @@ -199,41 +198,6 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { fold(constant([]), identity) ); } - - public async getLogItem( - requestContext: RequestHandlerContext, - id: string, - sourceConfiguration: InfraSourceConfiguration - ) { - const search = (searchOptions: object) => - this.framework.callWithRequest<LogItemHit, {}>(requestContext, 'search', searchOptions); - - const params = { - index: sourceConfiguration.logAlias, - terminate_after: 1, - body: { - size: 1, - sort: [ - { [sourceConfiguration.fields.timestamp]: 'desc' }, - { [sourceConfiguration.fields.tiebreaker]: 'desc' }, - ], - query: { - ids: { - values: [id], - }, - }, - fields: ['*'], - _source: false, - }, - }; - - const response = await search(params); - const document = first(response.hits.hits); - if (!document) { - throw new Error('Document not found'); - } - return document; - } } function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): LogEntryDocument[] { diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts deleted file mode 100644 index 7b79a1bf0386a..0000000000000 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields'; - -describe('convertESFieldsToLogItemFields', () => { - test('Converts the fields collection to LogItemFields', () => { - const esFields = { - 'agent.hostname': ['demo-stack-client-01'], - 'agent.id': ['7adef8b6-2ab7-45cd-a0d5-b3baad735f1b'], - 'agent.type': ['filebeat'], - 'agent.ephemeral_id': ['a0c8164b-3564-4e32-b0bf-f4db5a7ae566'], - 'agent.version': ['7.0.0'], - tags: ['prod', 'web'], - metadata: [ - { key: 'env', value: 'prod' }, - { key: 'stack', value: 'web' }, - ], - 'host.hostname': ['packer-virtualbox-iso-1546820004'], - 'host.name': ['demo-stack-client-01'], - }; - - const fields = convertESFieldsToLogItemFields(esFields); - expect(fields).toEqual([ - { - field: 'agent.hostname', - value: ['demo-stack-client-01'], - }, - { - field: 'agent.id', - value: ['7adef8b6-2ab7-45cd-a0d5-b3baad735f1b'], - }, - { - field: 'agent.type', - value: ['filebeat'], - }, - { - field: 'agent.ephemeral_id', - value: ['a0c8164b-3564-4e32-b0bf-f4db5a7ae566'], - }, - { - field: 'agent.version', - value: ['7.0.0'], - }, - { - field: 'tags', - value: ['prod', 'web'], - }, - { - field: 'metadata', - value: ['{"key":"env","value":"prod"}', '{"key":"stack","value":"web"}'], - }, - { - field: 'host.hostname', - value: ['packer-virtualbox-iso-1546820004'], - }, - { - field: 'host.name', - value: ['demo-stack-client-01'], - }, - ]); - }); -}); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts deleted file mode 100644 index a1d855bfdaa48..0000000000000 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import stringify from 'json-stable-stringify'; -import { LogEntriesItemField } from '../../../../common/http_api'; -import { JsonArray } from '../../../../common/typed_json'; - -const serializeValue = (value: JsonArray): string[] => { - return value.map((v) => { - if (typeof v === 'object' && v != null) { - return stringify(v); - } else { - return `${v}`; - } - }); -}; - -export const convertESFieldsToLogItemFields = (fields: { - [field: string]: JsonArray; -}): LogEntriesItemField[] => { - return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field]) })); -}; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index e10eb1d7e8aad..52cf6f46716b3 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -4,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; - import { RequestHandlerContext } from 'src/core/server'; -import { JsonArray, JsonObject } from '../../../../common/typed_json'; +import { JsonObject } from '../../../../common/typed_json'; import { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket, LogEntry, - LogEntriesItem, - LogEntriesCursor, LogColumn, LogEntriesRequest, } from '../../../../common/http_api'; @@ -23,7 +19,6 @@ import { SavedSourceConfigurationFieldColumnRuntimeType, } from '../../sources'; import { getBuiltinRules } from './builtin_rules'; -import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields'; import { CompiledLogMessageFormattingRule, Fields, @@ -38,20 +33,21 @@ import { CompositeDatasetKey, createLogEntryDatasetsQuery, } from './queries/log_entry_datasets'; +import { LogEntryCursor } from '../../../../common/log_entry'; export interface LogEntriesParams { startTimestamp: number; endTimestamp: number; size?: number; query?: JsonObject; - cursor?: { before: LogEntriesCursor | 'last' } | { after: LogEntriesCursor | 'first' }; + cursor?: { before: LogEntryCursor | 'last' } | { after: LogEntryCursor | 'first' }; highlightTerm?: string; } export interface LogEntriesAroundParams { startTimestamp: number; endTimestamp: number; size?: number; - center: LogEntriesCursor; + center: LogEntryCursor; query?: JsonObject; highlightTerm?: string; } @@ -259,31 +255,6 @@ export class InfraLogEntriesDomain { return summaries; } - public async getLogItem( - requestContext: RequestHandlerContext, - id: string, - sourceConfiguration: InfraSourceConfiguration - ): Promise<LogEntriesItem> { - const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration); - const defaultFields = [ - { field: '_index', value: [document._index] }, - { field: '_id', value: [document._id] }, - ]; - - return { - id: document._id, - index: document._index, - key: { - time: document.sort[0], - tiebreaker: document.sort[1], - }, - fields: sortBy( - [...defaultFields, ...convertESFieldsToLogItemFields(document.fields)], - 'field' - ), - }; - } - public async getLogEntryDatasets( requestContext: RequestHandlerContext, timestampField: string, @@ -324,13 +295,6 @@ export class InfraLogEntriesDomain { } } -export interface LogItemHit { - _index: string; - _id: string; - fields: { [field: string]: [value: JsonArray] }; - sort: [number, number]; -} - export interface LogEntriesAdapter { getLogEntries( requestContext: RequestHandlerContext, @@ -347,12 +311,6 @@ export interface LogEntriesAdapter { bucketSize: number, filterQuery?: LogEntryQuery ): Promise<LogSummaryBucket[]>; - - getLogItem( - requestContext: RequestHandlerContext, - id: string, - source: InfraSourceConfiguration - ): Promise<LogItemHit>; } export type LogEntryQuery = JsonObject; @@ -361,14 +319,14 @@ export interface LogEntryDocument { id: string; fields: Fields; highlights: Highlights; - cursor: LogEntriesCursor; + cursor: LogEntryCursor; } export interface LogSummaryBucket { entriesCount: number; start: number; end: number; - topEntryKeys: LogEntriesCursor[]; + topEntryKeys: LogEntryCursor[]; } const logSummaryBucketHasEntries = (bucket: LogSummaryBucket) => diff --git a/x-pack/plugins/infra/server/lib/sources/mocks.ts b/x-pack/plugins/infra/server/lib/sources/mocks.ts new file mode 100644 index 0000000000000..c48340e87a631 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/mocks.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { InfraSources } from './sources'; + +type IInfraSources = Pick<InfraSources, keyof InfraSources>; + +export const createInfraSourcesMock = (): jest.Mocked<IInfraSources> => ({ + getSourceConfiguration: jest.fn(), + createSourceConfiguration: jest.fn(), + deleteSourceConfiguration: jest.fn(), + updateSourceConfiguration: jest.fn(), + getAllSourceConfigurations: jest.fn(), + getInternalSourceConfiguration: jest.fn(), + defineInternalSourceConfiguration: jest.fn(), +}); diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 65acc2b2756bd..d144b079b41e8 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -28,6 +28,9 @@ interface Libs { config: InfraConfig; } +// extract public interface +export type IInfraSources = Pick<InfraSources, keyof InfraSources>; + export class InfraSources { private internalSourceConfigurations: Map<string, InfraStaticSourceConfiguration> = new Map(); private readonly libs: Libs; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index ef09dbfcb2674..693e98521ada2 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -4,32 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Server } from '@hapi/hapi'; -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { Observable } from 'rxjs'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; +import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; +import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; +import { LOGS_FEATURE, METRICS_FEATURE } from './features'; import { initInfraServer } from './infra_server'; -import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter'; +import { InfraServerPluginSetupDeps, InfraServerPluginStartDeps } from './lib/adapters/framework'; import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status'; +import { registerAlertTypes } from './lib/alerting'; import { InfraFieldsDomain } from './lib/domains/fields_domain'; import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; import { InfraMetricsDomain } from './lib/domains/metrics_domain'; +import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; +import { infraSourceConfigurationSavedObjectType, InfraSources } from './lib/sources'; import { InfraSourceStatus } from './lib/source_status'; -import { InfraSources } from './lib/sources'; -import { InfraServerPluginDeps } from './lib/adapters/framework'; -import { METRICS_FEATURE, LOGS_FEATURE } from './features'; -import { UsageCollector } from './usage/usage_collector'; -import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; -import { registerAlertTypes } from './lib/alerting'; -import { infraSourceConfigurationSavedObjectType } from './lib/sources'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; +import { LogEntriesService } from './services/log_entries'; import { InfraRequestHandlerContext } from './types'; +import { UsageCollector } from './usage/usage_collector'; export const config = { schema: schema.object({ @@ -87,7 +87,7 @@ export class InfraServerPlugin { this.config$ = context.config.create<InfraConfig>(); } - async setup(core: CoreSetup, plugins: InfraServerPluginDeps) { + async setup(core: CoreSetup<InfraServerPluginStartDeps>, plugins: InfraServerPluginSetupDeps) { await new Promise<void>((resolve) => { this.config$.subscribe((configValue) => { this.config = configValue; @@ -167,6 +167,9 @@ export class InfraServerPlugin { // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); + const logEntriesService = new LogEntriesService(); + logEntriesService.setup(core, { ...plugins, sources }); + return { defineInternalSourceConfiguration(sourceId, sourceProperties) { sources.defineInternalSourceConfiguration(sourceId, sourceProperties); diff --git a/x-pack/plugins/infra/server/routes/log_entries/index.ts b/x-pack/plugins/infra/server/routes/log_entries/index.ts index 1090d35d89b85..9e34c1fc91199 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/index.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/index.ts @@ -6,6 +6,5 @@ export * from './entries'; export * from './highlights'; -export * from './item'; export * from './summary'; export * from './summary_highlights'; diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts deleted file mode 100644 index 67ca481ff4fcb..0000000000000 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createValidationFunction } from '../../../common/runtime_types'; - -import { InfraBackendLibs } from '../../lib/infra_types'; -import { - LOG_ENTRIES_ITEM_PATH, - logEntriesItemRequestRT, - logEntriesItemResponseRT, -} from '../../../common/http_api'; - -export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'post', - path: LOG_ENTRIES_ITEM_PATH, - validate: { body: createValidationFunction(logEntriesItemRequestRT) }, - }, - async (requestContext, request, response) => { - try { - const payload = request.body; - const { id, sourceId } = payload; - const sourceConfiguration = ( - await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId) - ).configuration; - - const logEntry = await logEntries.getLogItem(requestContext, id, sourceConfiguration); - - return response.ok({ - body: logEntriesItemResponseRT.encode({ - data: logEntry, - }), - }); - } catch (error) { - return response.internalError({ body: error.message }); - } - } - ); -}; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/common.ts b/x-pack/plugins/infra/server/services/log_entries/index.ts similarity index 55% rename from x-pack/plugins/infra/common/http_api/log_entries/common.ts rename to x-pack/plugins/infra/server/services/log_entries/index.ts index 0b31222322007..90b97b924fa0d 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/common.ts +++ b/x-pack/plugins/infra/server/services/log_entries/index.ts @@ -4,10 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as rt from 'io-ts'; - -export const logEntriesCursorRT = rt.type({ - time: rt.number, - tiebreaker: rt.number, -}); -export type LogEntriesCursor = rt.TypeOf<typeof logEntriesCursorRT>; +export * from './log_entries_service'; diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_service.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_service.ts new file mode 100644 index 0000000000000..edd53be9db841 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_service.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/server'; +import { LOG_ENTRY_SEARCH_STRATEGY } from '../../../common/search_strategies/log_entries/log_entry'; +import { logEntrySearchStrategyProvider } from './log_entry_search_strategy'; +import { LogEntriesServiceSetupDeps, LogEntriesServiceStartDeps } from './types'; + +export class LogEntriesService { + public setup(core: CoreSetup<LogEntriesServiceStartDeps>, setupDeps: LogEntriesServiceSetupDeps) { + core.getStartServices().then(([, startDeps]) => { + setupDeps.data.search.registerSearchStrategy( + LOG_ENTRY_SEARCH_STRATEGY, + logEntrySearchStrategyProvider({ ...setupDeps, ...startDeps }) + ); + }); + } + + public start(_startDeps: LogEntriesServiceStartDeps) {} +} diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts new file mode 100644 index 0000000000000..044cea3899baf --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -0,0 +1,225 @@ +/* + * 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 { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { of, throwError } from 'rxjs'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, + uiSettingsServiceMock, +} from 'src/core/server/mocks'; +import { + IEsSearchRequest, + IEsSearchResponse, + ISearchStrategy, + SearchStrategyDependencies, +} from 'src/plugins/data/server'; +import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { + logEntrySearchRequestStateRT, + logEntrySearchStrategyProvider, +} from './log_entry_search_strategy'; + +describe('LogEntry search strategy', () => { + it('handles initial search requests', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: true, + rawResponse: { + took: 0, + _shards: { total: 1, failed: 0, skipped: 0, successful: 0 }, + timed_out: false, + hits: { total: 0, max_score: 0, hits: [] }, + }, + }); + + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + + const response = await logEntrySearchStrategy + .search( + { + params: { sourceId: 'SOURCE_ID', logEntryId: 'LOG_ENTRY_ID' }, + }, + {}, + mockDependencies + ) + .toPromise(); + + expect(sourcesMock.getSourceConfiguration).toHaveBeenCalled(); + expect(esSearchStrategyMock.search).toHaveBeenCalled(); + expect(response.id).toEqual(expect.any(String)); + expect(response.isRunning).toBe(true); + }); + + it('handles subsequent polling requests', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: false, + rawResponse: { + took: 1, + _shards: { total: 1, failed: 0, skipped: 0, successful: 1 }, + timed_out: false, + hits: { + total: 0, + max_score: 0, + hits: [ + { + _id: 'HIT_ID', + _index: 'HIT_INDEX', + _type: '_doc', + _score: 0, + _source: null, + fields: { + '@timestamp': [1605116827143], + message: ['HIT_MESSAGE'], + }, + sort: [1605116827143 as any, 1 as any], // incorrectly typed as string upstream + }, + ], + }, + }, + }); + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + const requestId = logEntrySearchRequestStateRT.encode({ + esRequestId: 'ASYNC_REQUEST_ID', + }); + + const response = await logEntrySearchStrategy + .search( + { + id: requestId, + params: { sourceId: 'SOURCE_ID', logEntryId: 'LOG_ENTRY_ID' }, + }, + {}, + mockDependencies + ) + .toPromise(); + + expect(sourcesMock.getSourceConfiguration).not.toHaveBeenCalled(); + expect(esSearchStrategyMock.search).toHaveBeenCalled(); + expect(response.id).toEqual(requestId); + expect(response.isRunning).toBe(false); + expect(response.rawResponse.data).toEqual({ + id: 'HIT_ID', + index: 'HIT_INDEX', + key: { + time: 1605116827143, + tiebreaker: 1, + }, + fields: [ + { field: '@timestamp', value: [1605116827143] }, + { field: 'message', value: ['HIT_MESSAGE'] }, + ], + }); + }); + + it('forwards errors from the underlying search strategy', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: false, + rawResponse: { + took: 1, + _shards: { total: 1, failed: 0, skipped: 0, successful: 1 }, + timed_out: false, + hits: { total: 0, max_score: 0, hits: [] }, + }, + }); + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + + const response = logEntrySearchStrategy.search( + { + id: logEntrySearchRequestStateRT.encode({ esRequestId: 'UNKNOWN_ID' }), + params: { sourceId: 'SOURCE_ID', logEntryId: 'LOG_ENTRY_ID' }, + }, + {}, + mockDependencies + ); + + await expect(response.toPromise()).rejects.toThrowError(ResponseError); + }); +}); + +const createSourceConfigurationMock = () => ({ + id: 'SOURCE_ID', + origin: 'stored' as const, + configuration: { + name: 'SOURCE_NAME', + description: 'SOURCE_DESCRIPTION', + logAlias: 'log-indices-*', + metricAlias: 'metric-indices-*', + inventoryDefaultView: 'DEFAULT_VIEW', + metricsExplorerDefaultView: 'DEFAULT_VIEW', + logColumns: [], + fields: { + pod: 'POD_FIELD', + host: 'HOST_FIELD', + container: 'CONTAINER_FIELD', + message: ['MESSAGE_FIELD'], + timestamp: 'TIMESTAMP_FIELD', + tiebreaker: 'TIEBREAKER_FIELD', + }, + }, +}); + +const createEsSearchStrategyMock = (esSearchResponse: IEsSearchResponse) => ({ + search: jest.fn((esSearchRequest: IEsSearchRequest) => { + if (typeof esSearchRequest.id === 'string') { + if (esSearchRequest.id === esSearchResponse.id) { + return of(esSearchResponse); + } else { + return throwError( + new ResponseError({ + body: {}, + headers: {}, + meta: {} as any, + statusCode: 404, + warnings: [], + }) + ); + } + } else { + return of(esSearchResponse); + } + }), +}); + +const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => ({ + uiSettingsClient: uiSettingsServiceMock.createClient(), + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), +}); + +// using the official data mock from within x-pack doesn't type-check successfully, +// because the `licensing` plugin modifies the `RequestHandlerContext` core type. +const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ + search: { + getSearchStrategy: jest.fn().mockReturnValue(esSearchStrategyMock), + }, +}); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts new file mode 100644 index 0000000000000..a0dfe3d7176fd --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { concat, defer, of } from 'rxjs'; +import { concatMap, filter, map, shareReplay, take } from 'rxjs/operators'; +import type { + IEsSearchRequest, + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../src/plugins/data/common'; +import type { + ISearchStrategy, + PluginStart as DataPluginStart, +} from '../../../../../../src/plugins/data/server'; +import { getLogEntryCursorFromHit } from '../../../common/log_entry'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { + LogEntrySearchRequestParams, + logEntrySearchRequestParamsRT, + LogEntrySearchResponsePayload, + logEntrySearchResponsePayloadRT, +} from '../../../common/search_strategies/log_entries/log_entry'; +import type { IInfraSources } from '../../lib/sources'; +import { + createAsyncRequestRTs, + createErrorFromShardFailure, + jsonFromBase64StringRT, +} from '../../utils/typed_search_strategy'; +import { createGetLogEntryQuery, getLogEntryResponseRT, LogEntryHit } from './queries/log_entry'; + +type LogEntrySearchRequest = IKibanaSearchRequest<LogEntrySearchRequestParams>; +type LogEntrySearchResponse = IKibanaSearchResponse<LogEntrySearchResponsePayload>; + +export const logEntrySearchStrategyProvider = ({ + data, + sources, +}: { + data: DataPluginStart; + sources: IInfraSources; +}): ISearchStrategy<LogEntrySearchRequest, LogEntrySearchResponse> => { + const esSearchStrategy = data.search.getSearchStrategy('ese'); + + return { + search: (rawRequest, options, dependencies) => + defer(() => { + const request = decodeOrThrow(asyncRequestRT)(rawRequest); + + const sourceConfiguration$ = defer(() => + sources.getSourceConfiguration(dependencies.savedObjectsClient, request.params.sourceId) + ).pipe(shareReplay(1)); + + const recoveredRequest$ = of(request).pipe( + filter(asyncRecoveredRequestRT.is), + map(({ id: { esRequestId } }) => ({ id: esRequestId })) + ); + + const initialRequest$ = of(request).pipe( + filter(asyncInitialRequestRT.is), + concatMap(({ params }) => + sourceConfiguration$.pipe( + map( + ({ configuration }): IEsSearchRequest => ({ + params: createGetLogEntryQuery( + configuration.logAlias, + params.logEntryId, + configuration.fields.timestamp, + configuration.fields.tiebreaker + ), + }) + ) + ) + ) + ); + + return concat(recoveredRequest$, initialRequest$).pipe( + take(1), + concatMap((esRequest) => esSearchStrategy.search(esRequest, options, dependencies)), + map((esResponse) => ({ + ...esResponse, + rawResponse: decodeOrThrow(getLogEntryResponseRT)(esResponse.rawResponse), + })), + map((esResponse) => ({ + ...esResponse, + ...(esResponse.id + ? { id: logEntrySearchRequestStateRT.encode({ esRequestId: esResponse.id }) } + : {}), + rawResponse: logEntrySearchResponsePayloadRT.encode({ + data: esResponse.rawResponse.hits.hits.map(createLogEntryFromHit)[0] ?? null, + errors: (esResponse.rawResponse._shards.failures ?? []).map( + createErrorFromShardFailure + ), + }), + })) + ); + }), + cancel: async (id, options, dependencies) => { + const { esRequestId } = decodeOrThrow(logEntrySearchRequestStateRT)(id); + return await esSearchStrategy.cancel?.(esRequestId, options, dependencies); + }, + }; +}; + +// exported for tests +export const logEntrySearchRequestStateRT = rt.string.pipe(jsonFromBase64StringRT).pipe( + rt.type({ + esRequestId: rt.string, + }) +); + +const { asyncInitialRequestRT, asyncRecoveredRequestRT, asyncRequestRT } = createAsyncRequestRTs( + logEntrySearchRequestStateRT, + logEntrySearchRequestParamsRT +); + +const createLogEntryFromHit = (hit: LogEntryHit) => ({ + id: hit._id, + index: hit._index, + key: getLogEntryCursorFromHit(hit), + fields: Object.entries(hit.fields).map(([field, value]) => ({ field, value })), +}); diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts new file mode 100644 index 0000000000000..880a48fd5b8f7 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { RequestParams } from '@elastic/elasticsearch'; +import * as rt from 'io-ts'; +import { jsonArrayRT } from '../../../../common/typed_json'; +import { + commonHitFieldsRT, + commonSearchSuccessResponseFieldsRT, +} from '../../../utils/elasticsearch_runtime_types'; + +export const createGetLogEntryQuery = ( + logEntryIndex: string, + logEntryId: string, + timestampField: string, + tiebreakerField: string +): RequestParams.Search<Record<string, any>> => ({ + index: logEntryIndex, + terminate_after: 1, + track_scores: false, + track_total_hits: false, + body: { + size: 1, + query: { + ids: { + values: [logEntryId], + }, + }, + fields: ['*'], + sort: [{ [timestampField]: 'desc' }, { [tiebreakerField]: 'desc' }], + _source: false, + }, +}); + +export const logEntryHitRT = rt.intersection([ + commonHitFieldsRT, + rt.type({ + fields: rt.record(rt.string, jsonArrayRT), + sort: rt.tuple([rt.number, rt.number]), + }), +]); + +export type LogEntryHit = rt.TypeOf<typeof logEntryHitRT>; + +export const getLogEntryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + hits: rt.array(logEntryHitRT), + }), + }), +]); + +export type GetLogEntryResponse = rt.TypeOf<typeof getLogEntryResponseRT>; diff --git a/x-pack/plugins/infra/server/services/log_entries/types.ts b/x-pack/plugins/infra/server/services/log_entries/types.ts new file mode 100644 index 0000000000000..d9f1024845bad --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../../../src/plugins/data/server'; +import { InfraSources } from '../../lib/sources'; + +export interface LogEntriesServiceSetupDeps { + data: DataPluginSetup; + sources: InfraSources; +} + +export interface LogEntriesServiceStartDeps { + data: DataPluginStart; +} diff --git a/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts b/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts index a48c65d648b25..271dbb864abad 100644 --- a/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts +++ b/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts @@ -6,13 +6,35 @@ import * as rt from 'io-ts'; -export const commonSearchSuccessResponseFieldsRT = rt.type({ - _shards: rt.type({ - total: rt.number, - successful: rt.number, - skipped: rt.number, - failed: rt.number, +export const shardFailureRT = rt.type({ + index: rt.string, + node: rt.string, + reason: rt.type({ + reason: rt.string, + type: rt.string, }), + shard: rt.number, +}); + +export type ShardFailure = rt.TypeOf<typeof shardFailureRT>; + +export const commonSearchSuccessResponseFieldsRT = rt.type({ + _shards: rt.intersection([ + rt.type({ + total: rt.number, + successful: rt.number, + skipped: rt.number, + failed: rt.number, + }), + rt.partial({ + failures: rt.array(shardFailureRT), + }), + ]), timed_out: rt.boolean, took: rt.number, }); + +export const commonHitFieldsRT = rt.type({ + _index: rt.string, + _id: rt.string, +}); diff --git a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts new file mode 100644 index 0000000000000..1234aea507f3f --- /dev/null +++ b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import stringify from 'json-stable-stringify'; +import { JsonValue, jsonValueRT } from '../../common/typed_json'; +import { SearchStrategyError } from '../../common/search_strategies/common/errors'; +import { ShardFailure } from './elasticsearch_runtime_types'; + +export const jsonFromBase64StringRT = new rt.Type<JsonValue, string, string>( + 'JSONFromBase64String', + jsonValueRT.is, + (value, context) => { + try { + return rt.success(JSON.parse(Buffer.from(value, 'base64').toString())); + } catch (error) { + return rt.failure(error, context); + } + }, + (a) => Buffer.from(stringify(a)).toString('base64') +); + +export const createAsyncRequestRTs = <StateCodec extends rt.Mixed, ParamsCodec extends rt.Mixed>( + stateCodec: StateCodec, + paramsCodec: ParamsCodec +) => { + const asyncRecoveredRequestRT = rt.type({ + id: stateCodec, + params: paramsCodec, + }); + + const asyncInitialRequestRT = rt.type({ + id: rt.undefined, + params: paramsCodec, + }); + + const asyncRequestRT = rt.union([asyncRecoveredRequestRT, asyncInitialRequestRT]); + + return { + asyncInitialRequestRT, + asyncRecoveredRequestRT, + asyncRequestRT, + }; +}; + +export const createErrorFromShardFailure = (failure: ShardFailure): SearchStrategyError => ({ + type: 'shardFailure' as const, + shardInfo: { + index: failure.index, + node: failure.node, + shard: failure.shard, + }, + message: failure.reason.reason, +}); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7b84c62264c83..297ed96a6e494 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9341,7 +9341,6 @@ "xpack.infra.logEntryItemView.logEntryActionsMenuToolTip": "行のアクションを表示", "xpack.infra.logFlyout.fieldColumnLabel": "フィールド", "xpack.infra.logFlyout.filterAriaLabel": "フィルター", - "xpack.infra.logFlyout.flyoutTitle": "ログイベントドキュメントの詳細", "xpack.infra.logFlyout.loadingMessage": "イベントを読み込み中", "xpack.infra.logFlyout.setFilterTooltip": "フィルターでイベントを表示", "xpack.infra.logFlyout.valueColumnLabel": "値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 55071303a1b36..3e8b5d0996ee7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9350,7 +9350,6 @@ "xpack.infra.logEntryItemView.logEntryActionsMenuToolTip": "查看适用于以下行的操作:", "xpack.infra.logFlyout.fieldColumnLabel": "字段", "xpack.infra.logFlyout.filterAriaLabel": "筛选", - "xpack.infra.logFlyout.flyoutTitle": "日志事件文档详情", "xpack.infra.logFlyout.loadingMessage": "正在加载事件", "xpack.infra.logFlyout.setFilterTooltip": "使用筛选查看事件", "xpack.infra.logFlyout.valueColumnLabel": "值", diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index fdd37fa4c335c..819a2d35b92a6 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -16,7 +16,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./sources')); loadTestFile(require.resolve('./snapshot')); - loadTestFile(require.resolve('./log_item')); loadTestFile(require.resolve('./metrics_alerting')); loadTestFile(require.resolve('./metrics_explorer')); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_item.ts b/x-pack/test/api_integration/apis/metrics_ui/log_item.ts deleted file mode 100644 index 3bb7a9a76690d..0000000000000 --- a/x-pack/test/api_integration/apis/metrics_ui/log_item.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -import { - LOG_ENTRIES_ITEM_PATH, - logEntriesItemRequestRT, -} from '../../../../plugins/infra/common/http_api'; - -const COMMON_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', -}; - -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - - describe('Log Item Endpoint', () => { - before(() => esArchiver.load('infra/metrics_and_logs')); - after(() => esArchiver.unload('infra/metrics_and_logs')); - - it('should basically work', async () => { - const { body } = await supertest - .post(LOG_ENTRIES_ITEM_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesItemRequestRT.encode({ - sourceId: 'default', - id: 'yT2Mg2YBh-opCxJv8Vqj', - }) - ) - .expect(200); - - const logItem = body.data; - - expect(logItem).to.have.property('id', 'yT2Mg2YBh-opCxJv8Vqj'); - expect(logItem).to.have.property('index', 'filebeat-7.0.0-alpha1-2018.10.17'); - expect(logItem).to.have.property('fields'); - expect(logItem.fields).to.eql([ - { - field: '@timestamp', - value: ['2018-10-17T19:42:22.000Z'], - }, - { - field: '_id', - value: ['yT2Mg2YBh-opCxJv8Vqj'], - }, - { - field: '_index', - value: ['filebeat-7.0.0-alpha1-2018.10.17'], - }, - { - field: 'apache2.access.body_sent.bytes', - value: ['1336'], - }, - { - field: 'apache2.access.http_version', - value: ['1.1'], - }, - { - field: 'apache2.access.method', - value: ['GET'], - }, - { - field: 'apache2.access.referrer', - value: ['-'], - }, - { - field: 'apache2.access.remote_ip', - value: ['10.128.0.11'], - }, - { - field: 'apache2.access.response_code', - value: ['200'], - }, - { - field: 'apache2.access.url', - value: ['/a-fresh-start-will-put-you-on-your-way'], - }, - { - field: 'apache2.access.user_agent.device', - value: ['Other'], - }, - { - field: 'apache2.access.user_agent.name', - value: ['Other'], - }, - { - field: 'apache2.access.user_agent.os', - value: ['Other'], - }, - { - field: 'apache2.access.user_agent.os_name', - value: ['Other'], - }, - { - field: 'apache2.access.user_name', - value: ['-'], - }, - { - field: 'beat.hostname', - value: ['demo-stack-apache-01'], - }, - { - field: 'beat.name', - value: ['demo-stack-apache-01'], - }, - { - field: 'beat.version', - value: ['7.0.0-alpha1'], - }, - { - field: 'fileset.module', - value: ['apache2'], - }, - { - field: 'fileset.name', - value: ['access'], - }, - { - field: 'host.name', - value: ['demo-stack-apache-01'], - }, - { - field: 'input.type', - value: ['log'], - }, - { - field: 'offset', - value: ['5497614'], - }, - { - field: 'prospector.type', - value: ['log'], - }, - { - field: 'read_timestamp', - value: ['2018-10-17T19:42:23.160Z'], - }, - { - field: 'source', - value: ['/var/log/apache2/access.log'], - }, - ]); - }); - }); -} From 487eb2e4e4ddf232297e5d58177fc1477b6fb76a Mon Sep 17 00:00:00 2001 From: Marta Bondyra <marta.bondyra@gmail.com> Date: Thu, 3 Dec 2020 13:28:41 +0100 Subject: [PATCH 095/107] [Lens] accessibility screen reader issues (#84395) * [Lens] accessibility screen reader issues * fix i18n * fix: no aria-label on divs * cr fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../config_panel/config_panel.tsx | 2 +- .../config_panel/dimension_container.scss | 36 ++++++- .../config_panel/dimension_container.tsx | 54 +++++++---- .../config_panel/layer_panel.test.tsx | 2 +- .../editor_frame/config_panel/layer_panel.tsx | 54 ++++++++--- .../workspace_panel_wrapper.tsx | 4 +- .../indexpattern_datasource/field_item.tsx | 93 ++++++++++--------- .../indexpattern_datasource/field_list.tsx | 28 +++--- .../fields_accordion.tsx | 4 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../test/functional/page_objects/lens_page.ts | 2 +- 12 files changed, 184 insertions(+), 101 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 3d453cd078b7f..b1fe9174d6d68 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -94,7 +94,7 @@ function LayerPanels( {...props} key={layerId} layerId={layerId} - dataTestSubj={`lns-layerPanel-${index}`} + index={index} visualizationState={visualizationState} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss index bd2789cf645c7..5947d62540a0d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss @@ -13,7 +13,39 @@ animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance; } -.lnsDimensionContainer__footer, -.lnsDimensionContainer__header { +.lnsDimensionContainer__footer { padding: $euiSizeS; } + +.lnsDimensionContainer__header { + padding: $euiSizeS $euiSizeXS; +} + +.lnsDimensionContainer__headerTitle { + padding: $euiSizeS $euiSizeXS; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.lnsDimensionContainer__headerLink { + &:focus-within { + background-color: transparentize($euiColorVis1, .9); + + .lnsDimensionContainer__headerTitle { + text-decoration: underline; + } + } +} + +.lnsDimensionContainer__backIcon { + &:hover { + transform: none !important; // sass-lint:disable-line no-important + } + + &:focus { + background-color: transparent; + } +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx index 8f1b441d1d285..748079cc7a400 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx @@ -10,7 +10,9 @@ import { EuiFlyoutHeader, EuiFlyoutFooter, EuiTitle, + EuiButtonIcon, EuiButtonEmpty, + EuiFlexGroup, EuiFlexItem, EuiFocusTrap, EuiOutsideClickDetector, @@ -54,24 +56,42 @@ export function DimensionContainer({ className="lnsDimensionContainer" > <EuiFlyoutHeader hasBorder className="lnsDimensionContainer__header"> - <EuiTitle size="xs"> - <EuiButtonEmpty - onClick={closeFlyout} - data-test-subj="lns-indexPattern-dimensionContainerTitle" - id="lnsDimensionContainerTitle" - iconType="sortLeft" - flush="left" - > - <strong> - {i18n.translate('xpack.lens.configure.configurePanelTitle', { - defaultMessage: '{groupLabel} configuration', - values: { - groupLabel, - }, + <EuiFlexGroup + gutterSize="none" + alignItems="center" + className="lnsDimensionContainer__headerLink" + onClick={closeFlyout} + > + <EuiFlexItem grow={false}> + <EuiButtonIcon + color="text" + data-test-subj="lns-indexPattern-dimensionContainerBack" + className="lnsDimensionContainer__backIcon" + onClick={closeFlyout} + iconType="sortLeft" + aria-label={i18n.translate('xpack.lens.dimensionContainer.closeConfiguration', { + defaultMessage: 'Close configuration', })} - </strong> - </EuiButtonEmpty> - </EuiTitle> + /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiTitle size="xs"> + <h2 + id="lnsDimensionContainerTitle" + className="lnsDimensionContainer__headerTitle" + > + <strong> + {i18n.translate('xpack.lens.configure.configurePanelTitle', { + defaultMessage: '{groupLabel} configuration', + values: { + groupLabel, + }, + })} + </strong> + </h2> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlyoutHeader> <EuiFlexItem className="eui-yScrollWithShadows" grow={1}> {panel} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 37dc039df498b..f6cba87e9c6c6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -58,7 +58,7 @@ describe('LayerPanel', () => { onRemoveLayer: jest.fn(), dispatch: jest.fn(), core: coreMock.createStart(), - dataTestSubj: 'lns_layerPanel-0', + index: 0, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index cc456e843bb68..4231f4b539977 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -29,6 +29,12 @@ import { DimensionContainer } from './dimension_container'; import { ColorIndicator } from './color_indicator'; import { PaletteIndicator } from './palette_indicator'; +const triggerLinkA11yText = (label: string) => + i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Click to edit configuration for {label} or drag to move', + values: { label }, + }); + const initialActiveDimensionState = { isNew: false, }; @@ -58,7 +64,7 @@ function isSameConfiguration(config1: unknown, config2: unknown) { export function LayerPanel( props: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & { layerId: string; - dataTestSubj: string; + index: number; isOnlyLayer: boolean; updateVisualization: StateSetter<unknown>; updateDatasource: (datasourceId: string, newState: unknown) => void; @@ -75,7 +81,7 @@ export function LayerPanel( initialActiveDimensionState ); - const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, dataTestSubj } = props; + const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, index } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; useEffect(() => { @@ -125,7 +131,11 @@ export function LayerPanel( const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); return ( <ChildDragDropProvider {...dragDropContext}> - <EuiPanel data-test-subj={dataTestSubj} className="lnsLayerPanel" paddingSize="s"> + <EuiPanel + data-test-subj={`lns-layerPanel-${index}`} + className="lnsLayerPanel" + paddingSize="s" + > <EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}> <EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem"> <LayerSettings @@ -180,14 +190,10 @@ export function LayerPanel( <EuiSpacer size="m" /> - {groups.map((group, index) => { + {groups.map((group, groupIndex) => { const newId = generateId(); const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Click to edit configuration or drag to move', - }); - return ( <EuiFormRow className={ @@ -198,7 +204,7 @@ export function LayerPanel( fullWidth label={<div className="lnsLayerPanel__groupLabel">{group.groupLabel}</div>} labelType="legend" - key={index} + key={groupIndex} isInvalid={isMissing} error={ isMissing ? ( @@ -327,8 +333,8 @@ export function LayerPanel( }); } }} - aria-label={triggerLinkA11yText} - title={triggerLinkA11yText} + aria-label={triggerLinkA11yText(columnLabelMap[accessor])} + title={triggerLinkA11yText(columnLabelMap[accessor])} > <ColorIndicator accessorConfig={accessorConfig}> <NativeRenderer @@ -351,11 +357,13 @@ export function LayerPanel( aria-label={i18n.translate( 'xpack.lens.indexPattern.removeColumnLabel', { - defaultMessage: 'Remove configuration', + defaultMessage: 'Remove configuration from "{groupLabel}"', + values: { groupLabel: group.groupLabel }, } )} title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { - defaultMessage: 'Remove configuration', + defaultMessage: 'Remove configuration from "{groupLabel}"', + values: { groupLabel: group.groupLabel }, })} onClick={() => { trackUiEvent('indexpattern_dimension_removed'); @@ -435,6 +443,13 @@ export function LayerPanel( contentProps={{ className: 'lnsLayerPanel__triggerTextContent', }} + aria-label={i18n.translate( + 'xpack.lens.indexPattern.removeColumnAriaLabel', + { + defaultMessage: 'Drop a field or click to add to {groupLabel}', + values: { groupLabel: group.groupLabel }, + } + )} data-test-subj="lns-empty-dimension" onClick={() => { if (activeId) { @@ -535,6 +550,17 @@ export function LayerPanel( iconType="trash" color="danger" data-test-subj="lnsLayerRemove" + aria-label={ + isOnlyLayer + ? i18n.translate('xpack.lens.resetLayerAriaLabel', { + defaultMessage: 'Reset layer {index}', + values: { index: index + 1 }, + }) + : i18n.translate('xpack.lens.deleteLayerAriaLabel', { + defaultMessage: `Delete layer {index}`, + values: { index: index + 1 }, + }) + } onClick={() => { // If we don't blur the remove / clear button, it remains focused // which is a strange UX in this case. e.target.blur doesn't work @@ -554,7 +580,7 @@ export function LayerPanel( defaultMessage: 'Reset layer', }) : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', + defaultMessage: `Delete layer`, })} </EuiButtonEmpty> </EuiFlexItem> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 046bebb33a57d..97a842f9e0243 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -124,7 +124,9 @@ export function WorkspacePanelWrapper({ <EuiScreenReaderOnly> <h1 id="lns_ChartTitle" data-test-subj="lns_ChartTitle"> {title || - i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} + i18n.translate('xpack.lens.chartTitle.unsaved', { + defaultMessage: 'Unsaved visualization', + })} </h1> </EuiScreenReaderOnly> <EuiPageContentBody className="lnsWorkspacePanelWrapper__pageContentBody"> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index fa4b5637f11f3..d070a01240b2e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -181,49 +181,56 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { /> ); return ( - <EuiPopover - ownFocus - className="lnsFieldItem__popoverAnchor" - display="block" - data-test-subj="lnsFieldListPanelField" - container={document.querySelector<HTMLElement>('.application') || undefined} - button={ - <DragDrop - label={field.displayName} - value={value} - data-test-subj={`lnsFieldListPanelField-${field.name}`} - draggable - > - <FieldButton - className={`lnsFieldItem lnsFieldItem--${field.type} lnsFieldItem--${ - exists ? 'exists' : 'missing' - }`} - isActive={infoIsOpen} - onClick={togglePopover} - aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonAriaLabel', { - defaultMessage: '{fieldName}: {fieldType}. Hit enter for a field preview.', - values: { - fieldName: field.displayName, - fieldType: field.type, - }, - })} - fieldIcon={lensFieldIcon} - fieldName={ - <EuiHighlight search={wrapOnDot(highlight)}> - {wrapOnDot(field.displayName)} - </EuiHighlight> - } - fieldInfoIcon={lensInfoIcon} - /> - </DragDrop> - } - isOpen={infoIsOpen} - closePopover={() => setOpen(false)} - anchorPosition="rightUp" - panelClassName="lnsFieldItem__fieldPanel" - > - <FieldItemPopoverContents {...state} {...props} /> - </EuiPopover> + <li> + <EuiPopover + ownFocus + className="lnsFieldItem__popoverAnchor" + display="block" + data-test-subj="lnsFieldListPanelField" + container={document.querySelector<HTMLElement>('.application') || undefined} + button={ + <DragDrop + label={field.displayName} + value={value} + data-test-subj={`lnsFieldListPanelField-${field.name}`} + draggable + > + <FieldButton + className={`lnsFieldItem lnsFieldItem--${field.type} lnsFieldItem--${ + exists ? 'exists' : 'missing' + }`} + isActive={infoIsOpen} + onClick={togglePopover} + buttonProps={{ + ['aria-label']: i18n.translate( + 'xpack.lens.indexPattern.fieldStatsButtonAriaLabel', + { + defaultMessage: '{fieldName}: {fieldType}. Hit enter for a field preview.', + values: { + fieldName: field.displayName, + fieldType: field.type, + }, + } + ), + }} + fieldIcon={lensFieldIcon} + fieldName={ + <EuiHighlight search={wrapOnDot(highlight)}> + {wrapOnDot(field.displayName)} + </EuiHighlight> + } + fieldInfoIcon={lensInfoIcon} + /> + </DragDrop> + } + isOpen={infoIsOpen} + closePopover={() => setOpen(false)} + anchorPosition="rightUp" + panelClassName="lnsFieldItem__fieldPanel" + > + <FieldItemPopoverContents {...state} {...props} /> + </EuiPopover> + </li> ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index 9e89468200e2c..16d1ecbf3296b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -125,19 +125,21 @@ export function FieldList({ onScroll={throttle(lazyScroll, 100)} > <div className="lnsIndexPatternFieldList__accordionContainer"> - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => !showInAccordion) - .flatMap(([, { fields }]) => - fields.map((field) => ( - <FieldItem - {...fieldProps} - exists={exists(field)} - field={field} - hideDetails={true} - key={field.name} - /> - )) - )} + <ul> + {Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => !showInAccordion) + .flatMap(([, { fields }]) => + fields.map((field) => ( + <FieldItem + {...fieldProps} + exists={exists(field)} + field={field} + hideDetails={true} + key={field.name} + /> + )) + )} + </ul> <EuiSpacer size="s" /> {Object.entries(fieldGroups) .filter(([, { showInAccordion }]) => showInAccordion) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index e531eb72f94ca..19f478c335784 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -113,9 +113,9 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ <EuiSpacer size="s" /> {hasLoaded && (!!fieldsCount ? ( - <div className="lnsInnerIndexPatternDataPanel__fieldItems"> + <ul className="lnsInnerIndexPatternDataPanel__fieldItems"> {paginatedFields && paginatedFields.map(renderField)} - </div> + </ul> ) : ( renderCallout ))} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 297ed96a6e494..43f18a2040dab 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10579,14 +10579,12 @@ "xpack.lens.chartSwitch.dataLossDescription": "このチャートに切り替えると構成の一部が失われます", "xpack.lens.chartSwitch.dataLossLabel": "データ喪失", "xpack.lens.chartSwitch.noResults": "{term}の結果が見つかりませんでした。", - "xpack.lens.chartTitle.unsaved": "未保存", "xpack.lens.configPanel.chartType": "チャートタイプ", "xpack.lens.configPanel.color.tooltip.auto": "カスタム色を指定しない場合、Lensは自動的に色を選択します。", "xpack.lens.configPanel.color.tooltip.custom": "[自動]モードに戻すには、カスタム色をオフにしてください。", "xpack.lens.configPanel.color.tooltip.disabled": "レイヤーに「内訳条件」が含まれている場合は、個別の系列をカスタム色にできません。", "xpack.lens.configPanel.selectVisualization": "ビジュアライゼーションを選択してください", "xpack.lens.configure.configurePanelTitle": "{groupLabel}構成", - "xpack.lens.configure.editConfig": "クリックして構成を編集するか、ドラッグして移動", "xpack.lens.configure.emptyConfig": "フィールドを破棄、またはクリックして追加", "xpack.lens.configure.invalidConfigTooltip": "無効な構成です。", "xpack.lens.configure.invalidConfigTooltipClick": "詳細はクリックしてください。", @@ -10725,7 +10723,6 @@ "xpack.lens.indexPattern.ranges.lessThanPrepend": "<", "xpack.lens.indexPattern.ranges.lessThanTooltip": "より小さい", "xpack.lens.indexPattern.records": "記録", - "xpack.lens.indexPattern.removeColumnLabel": "構成を削除", "xpack.lens.indexpattern.suggestions.nestingChangeLabel": "各 {outerOperation} の {innerOperation}", "xpack.lens.indexpattern.suggestions.overallLabel": "全体の {operation}", "xpack.lens.indexpattern.suggestions.overTimeLabel": "一定時間", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3e8b5d0996ee7..f11c9c8b39db7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10592,14 +10592,12 @@ "xpack.lens.chartSwitch.dataLossDescription": "切换到此图表将会丢失部分配置", "xpack.lens.chartSwitch.dataLossLabel": "数据丢失", "xpack.lens.chartSwitch.noResults": "找不到 {term} 的结果。", - "xpack.lens.chartTitle.unsaved": "未保存", "xpack.lens.configPanel.chartType": "图表类型", "xpack.lens.configPanel.color.tooltip.auto": "Lens 自动为您选取颜色,除非您指定定制颜色。", "xpack.lens.configPanel.color.tooltip.custom": "清除定制颜色以返回到“自动”模式。", "xpack.lens.configPanel.color.tooltip.disabled": "当图层包括“细分依据”,各个系列无法定制颜色。", "xpack.lens.configPanel.selectVisualization": "选择可视化", "xpack.lens.configure.configurePanelTitle": "{groupLabel} 配置", - "xpack.lens.configure.editConfig": "单击以编辑配置或进行拖移", "xpack.lens.configure.emptyConfig": "放置字段或单击以添加", "xpack.lens.configure.invalidConfigTooltip": "配置无效。", "xpack.lens.configure.invalidConfigTooltipClick": "单击了解更多详情。", @@ -10738,7 +10736,6 @@ "xpack.lens.indexPattern.ranges.lessThanPrepend": "<", "xpack.lens.indexPattern.ranges.lessThanTooltip": "小于", "xpack.lens.indexPattern.records": "记录", - "xpack.lens.indexPattern.removeColumnLabel": "移除配置", "xpack.lens.indexpattern.suggestions.nestingChangeLabel": "每个 {outerOperation} 的 {innerOperation}", "xpack.lens.indexpattern.suggestions.overallLabel": "{operation} - 总体", "xpack.lens.indexpattern.suggestions.overTimeLabel": "时移", diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index c22c3db0e4349..1f8ded1716ea1 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -204,7 +204,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont // closes the dimension editor flyout async closeDimensionEditor() { - await testSubjects.click('lns-indexPattern-dimensionContainerTitle'); + await testSubjects.click('lns-indexPattern-dimensionContainerBack'); }, /** From 145c0a512810da1f38d200a295c7bcb105b90c9f Mon Sep 17 00:00:00 2001 From: Marta Bondyra <marta.bondyra@gmail.com> Date: Thu, 3 Dec 2020 14:12:14 +0100 Subject: [PATCH 096/107] Revert "[Lens] (Accessibility) Focus mistakenly stops on righthand form (#84519)" (#84866) This reverts commit a9845c6fc2809a1d85a303aee6a4e07fefcd6581. --- .../editor_frame/config_panel/config_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index b1fe9174d6d68..c39c46c1f4152 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -88,7 +88,7 @@ function LayerPanels( const layerIds = activeVisualization.getLayerIds(visualizationState); return ( - <EuiForm className="lnsConfigPanel" tabIndex={-1}> + <EuiForm className="lnsConfigPanel"> {layerIds.map((layerId, index) => ( <LayerPanel {...props} From f9ade905a245e0d546662dda3dc54fe21e78d7f3 Mon Sep 17 00:00:00 2001 From: Brandon Kobel <brandon.kobel@elastic.co> Date: Thu, 3 Dec 2020 06:42:32 -0800 Subject: [PATCH 097/107] Deprecate `reporting.index` setting (#84005) * Deprecating `xpack.reporting.index` setting * Adding unit test * Now with more standard deprecation messages * Updating the xpack.reporting.index docs * Fixing tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/reporting-settings.asciidoc | 4 +- .../reporting/server/config/index.test.ts | 42 +++++++++++++++++++ .../plugins/reporting/server/config/index.ts | 10 +++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/reporting/server/config/index.test.ts diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index d44c42db92f41..2d91eb07c5236 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -261,7 +261,9 @@ For information about {kib} memory limits, see <<production, using {kib} in a pr [cols="2*<"] |=== | `xpack.reporting.index` - | Reporting uses a weekly index in {es} to store the reporting job and + | *deprecated* This setting is deprecated and will be removed in 8.0. Multitenancy by changing + `kibana.index` will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes] + for more details. Reporting uses a weekly index in {es} to store the reporting job and the report content. The index is automatically created if it does not already exist. Configure this to a unique value, beginning with `.reporting-`, for every {kib} instance that has a unique <<kibana-index, `kibana.index`>> setting. Defaults to `.reporting`. diff --git a/x-pack/plugins/reporting/server/config/index.test.ts b/x-pack/plugins/reporting/server/config/index.test.ts new file mode 100644 index 0000000000000..ac20ed6c303d7 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/index.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { config } from './index'; +import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; + +const CONFIG_PATH = 'xpack.reporting'; + +const applyReportingDeprecations = (settings: Record<string, any> = {}) => { + const deprecations = config.deprecations!(configDeprecationFactory); + const deprecationMessages: string[] = []; + const _config: any = {}; + _config[CONFIG_PATH] = settings; + const migrated = applyDeprecations( + _config, + deprecations.map((deprecation) => ({ + deprecation, + path: CONFIG_PATH, + })), + (msg) => deprecationMessages.push(msg) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('deprecations', () => { + ['.foo', '.reporting'].forEach((index) => { + it('logs a warning if index is set', () => { + const { messages } = applyReportingDeprecations({ index }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.reporting.index\\" is deprecated. Multitenancy by changing \\"kibana.index\\" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details", + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index b9c6f8e7591e3..9ec06df7e69b9 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { get } from 'lodash'; import { PluginConfigDescriptor } from 'kibana/server'; import { ConfigSchema, ReportingConfigType } from './schema'; export { buildConfig } from './config'; @@ -20,5 +21,14 @@ export const config: PluginConfigDescriptor<ReportingConfigType> = { unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), unused('poll.jobsRefresh.intervalErrorMultiplier'), unused('kibanaApp'), + (settings, fromPath, log) => { + const reporting = get(settings, fromPath); + if (reporting?.index) { + log( + `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details` + ); + } + return settings; + }, ], }; From 95f8d9d1ddcd6e85fb53ce31055aadd5a8ba99a7 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm <matthias.wilhelm@elastic.co> Date: Thu, 3 Dec 2020 15:42:45 +0100 Subject: [PATCH 098/107] [Discover] New responsive layout using EUI components (#83633) Co-authored-by: Andrea Del Rio <delrio.andre@gmail.com> Co-authored-by: cchaos <caroline.horn@elastic.co> Co-authored-by: Michail Yasonik <michail.yasonik@elastic.co> Co-authored-by: Dave Snider <dave.snider@gmail.com> Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co> --- .../public/application/_discover.scss | 162 ------- .../components/action_bar/action_bar.tsx | 3 +- .../angular/directives/fixed_scroll.js | 155 ------- .../angular/directives/fixed_scroll.test.js | 267 ----------- .../angular/directives/uninitialized.tsx | 57 +-- .../public/application/angular/discover.js | 4 +- .../angular/doc_table/infinite_scroll.ts | 25 +- .../angular/doc_table/lib/get_sort.ts | 10 +- .../context_app/context_app_legacy.scss | 5 - .../context_app/context_app_legacy.tsx | 24 +- .../application/components/discover.scss | 91 ++++ .../components/discover_legacy.tsx | 418 ++++++++++-------- .../public/application/components/doc/doc.tsx | 146 +++--- .../components/doc_viewer/doc_viewer.scss | 20 +- .../components/field_name/field_name.tsx | 4 +- .../components/hits_counter/hits_counter.scss | 3 + .../components/hits_counter/hits_counter.tsx | 4 +- .../loading_spinner/loading_spinner.scss | 4 + .../loading_spinner/loading_spinner.tsx | 4 +- .../components/no_results/_no_results.scss | 2 +- .../components/no_results/no_results.tsx | 3 +- .../sidebar/change_indexpattern.tsx | 17 +- .../components/sidebar/discover_field.tsx | 17 +- .../sidebar/discover_field_details.scss | 5 + .../sidebar/discover_field_search.scss | 7 + .../sidebar/discover_field_search.test.tsx | 9 +- .../sidebar/discover_field_search.tsx | 36 +- .../sidebar/discover_index_pattern.tsx | 39 +- .../components/sidebar/discover_sidebar.scss | 89 ++-- .../sidebar/discover_sidebar.test.tsx | 64 +-- .../components/sidebar/discover_sidebar.tsx | 393 +++++++++------- .../discover_sidebar_responsive.test.tsx | 145 ++++++ .../sidebar/discover_sidebar_responsive.tsx | 205 +++++++++ .../application/components/sidebar/index.ts | 1 + .../components/sidebar/lib/get_details.ts | 3 +- .../skip_bottom_button/skip_bottom_button.tsx | 40 +- .../application/components/table/table.tsx | 7 +- .../components/table/table_row.tsx | 41 +- .../table/table_row_btn_filter_add.tsx | 2 +- .../table/table_row_btn_filter_exists.tsx | 2 +- .../table/table_row_btn_filter_remove.tsx | 2 +- .../table/table_row_btn_toggle_column.tsx | 4 +- .../timechart_header/timechart_header.scss | 7 + .../timechart_header/timechart_header.tsx | 141 +++--- .../application/doc_views/doc_views_types.ts | 2 +- .../discover/public/application/index.scss | 1 - test/functional/apps/discover/_discover.js | 2 +- .../discover/{_sidebar.js => _sidebar.ts} | 35 +- test/functional/page_objects/discover_page.ts | 8 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 51 files changed, 1367 insertions(+), 1374 deletions(-) delete mode 100644 src/plugins/discover/public/application/_discover.scss delete mode 100644 src/plugins/discover/public/application/angular/directives/fixed_scroll.js delete mode 100644 src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js delete mode 100644 src/plugins/discover/public/application/components/context_app/context_app_legacy.scss create mode 100644 src/plugins/discover/public/application/components/discover.scss create mode 100644 src/plugins/discover/public/application/components/hits_counter/hits_counter.scss create mode 100644 src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss create mode 100644 src/plugins/discover/public/application/components/sidebar/discover_field_search.scss create mode 100644 src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx create mode 100644 src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx create mode 100644 src/plugins/discover/public/application/components/timechart_header/timechart_header.scss rename test/functional/apps/discover/{_sidebar.js => _sidebar.ts} (65%) diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss deleted file mode 100644 index bc704439d161b..0000000000000 --- a/src/plugins/discover/public/application/_discover.scss +++ /dev/null @@ -1,162 +0,0 @@ -.dscAppWrapper { - display: flex; - flex-direction: column; - flex-grow: 1; - overflow: hidden; -} - -.dscAppContainer { - > * { - position: relative; - } -} -discover-app { - flex-grow: 1; -} - -.dscHistogram { - display: flex; - height: 200px; - padding: $euiSizeS; -} - -// SASSTODO: replace the z-index value with a variable -.dscWrapper { - padding-left: $euiSizeXL; - padding-right: $euiSizeS; - z-index: 1; - @include euiBreakpoint('xs', 's', 'm') { - padding-left: $euiSizeS; - } -} - -@include euiPanel('.dscWrapper__content'); - -.dscWrapper__content { - padding-top: $euiSizeXS; - background-color: $euiColorEmptyShade; - - .kbn-table { - margin-bottom: 0; - } -} - -.dscTimechart { - display: block; - position: relative; - - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: 0.5; - stroke-width: 1; - } -} - -.dscResultCount { - padding-top: $euiSizeXS; -} - -.dscTimechart__header { - display: flex; - justify-content: center; - min-height: $euiSizeXXL; - padding: $euiSizeXS 0; -} - -.dscOverlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 20; - padding-top: $euiSizeM; - - opacity: 0.75; - text-align: center; - background-color: transparent; -} - -.dscTable { - overflow: auto; - - // SASSTODO: add a monospace modifier to the doc-table component - .kbnDocTable__row { - font-family: $euiCodeFontFamily; - font-size: $euiFontSizeXS; - } -} - -// SASSTODO: replace the padding value with a variable -.dscTable__footer { - background-color: $euiColorLightShade; - padding: 5px 10px; - text-align: center; -} - -.dscResults { - h3 { - margin: -20px 0 10px 0; - text-align: center; - } -} - -.dscResults__interval { - display: inline-block; - width: auto; -} - -.dscSkipButton { - position: absolute; - right: $euiSizeM; - top: $euiSizeXS; -} - -.dscTableFixedScroll { - overflow-x: auto; - padding-bottom: 0; - - + .dscTableFixedScroll__scroller { - position: fixed; - bottom: 0; - overflow-x: auto; - overflow-y: hidden; - } -} - -.dscCollapsibleSidebar { - position: relative; - z-index: $euiZLevel1; - - .dscCollapsibleSidebar__collapseButton { - position: absolute; - top: 0; - right: -$euiSizeXL + 4; - cursor: pointer; - z-index: -1; - min-height: $euiSizeM; - min-width: $euiSizeM; - padding: $euiSizeXS * .5; - } - - &.closed { - width: 0 !important; - border-right-width: 0; - border-left-width: 0; - .dscCollapsibleSidebar__collapseButton { - right: -$euiSizeL + 4; - } - } -} - -@include euiBreakpoint('xs', 's', 'm') { - .dscCollapsibleSidebar { - &.closed { - display: none; - } - - .dscCollapsibleSidebar__collapseButton { - display: none; - } - } -} diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx index d294ffca86341..14e43a8aa203c 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx @@ -119,7 +119,7 @@ export function ActionBar({ </EuiButtonEmpty> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiFormRow> + <EuiFormRow display="center"> <EuiFieldNumber aria-label={ isSuccessor @@ -130,6 +130,7 @@ export function ActionBar({ defaultMessage: 'Number of newer documents', }) } + compressed className="cxtSizePicker" data-test-subj={`${type}CountPicker`} disabled={isDisabled} diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.js b/src/plugins/discover/public/application/angular/directives/fixed_scroll.js deleted file mode 100644 index e2d5f10a0faf7..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/fixed_scroll.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import { createDebounceProviderTimeout } from './debounce'; - -const SCROLLER_HEIGHT = 20; - -/** - * This directive adds a fixed horizontal scrollbar to the bottom of the window that proxies its scroll events - * to the target element's real scrollbar. This is useful when the target element's horizontal scrollbar - * might be waaaay down the page, like the doc table on Discover. - */ -export function FixedScrollProvider($timeout) { - return { - restrict: 'A', - link: function ($scope, $el) { - return createFixedScroll($scope, $timeout)($el); - }, - }; -} - -export function createFixedScroll($scope, $timeout) { - const debounce = createDebounceProviderTimeout($timeout); - return function (el) { - const $el = typeof el.css === 'function' ? el : $(el); - let $window = $(window); - let $scroller = $('<div class="dscTableFixedScroll__scroller">').height(SCROLLER_HEIGHT); - - /** - * Remove the listeners bound in listen() - * @type {function} - */ - let unlisten = _.noop; - - /** - * Listen for scroll events on the $scroller and the $el, sets unlisten() - * - * unlisten must be called before calling or listen() will throw an Error - * - * Since the browser emits "scroll" events after setting scrollLeft - * the listeners also prevent tug-of-war - * - * @throws {Error} If unlisten was not called first - * @return {undefined} - */ - function listen() { - if (unlisten !== _.noop) { - throw new Error('fixedScroll listeners were not cleaned up properly before re-listening!'); - } - - let blockTo; - function bind($from, $to) { - function handler() { - if (blockTo === $to) return (blockTo = null); - $to.scrollLeft((blockTo = $from).scrollLeft()); - } - - $from.on('scroll', handler); - return function () { - $from.off('scroll', handler); - }; - } - - unlisten = _.flow(bind($el, $scroller), bind($scroller, $el), function () { - unlisten = _.noop; - }); - } - - /** - * Revert DOM changes and event listeners - * @return {undefined} - */ - function cleanUp() { - unlisten(); - $scroller.detach(); - $el.css('padding-bottom', 0); - } - - /** - * Modify the DOM and attach event listeners based on need. - * Is called many times to re-setup, must be idempotent - * @return {undefined} - */ - function setup() { - cleanUp(); - - const containerWidth = $el.width(); - const contentWidth = $el.prop('scrollWidth'); - const containerHorizOverflow = contentWidth - containerWidth; - - const elTop = $el.offset().top - $window.scrollTop(); - const elBottom = elTop + $el.height(); - const windowVertOverflow = elBottom - $window.height(); - - const requireScroller = containerHorizOverflow > 0 && windowVertOverflow > 0; - if (!requireScroller) return; - - // push the content away from the scroller - $el.css('padding-bottom', SCROLLER_HEIGHT); - - // fill the scroller with a dummy element that mimics the content - $scroller - .width(containerWidth) - .html($('<div>').css({ width: contentWidth, height: SCROLLER_HEIGHT })) - .insertAfter($el); - - // listen for scroll events - listen(); - } - - let width; - let scrollWidth; - function checkWidth() { - const newScrollWidth = $el.prop('scrollWidth'); - const newWidth = $el.width(); - - if (scrollWidth !== newScrollWidth || width !== newWidth) { - $scope.$apply(setup); - - scrollWidth = newScrollWidth; - width = newWidth; - } - } - - const debouncedCheckWidth = debounce(checkWidth, 100, { - invokeApply: false, - }); - $scope.$watch(debouncedCheckWidth); - - function destroy() { - cleanUp(); - debouncedCheckWidth.cancel(); - $scroller = $window = null; - } - return destroy; - }; -} diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js b/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js deleted file mode 100644 index e44bb45cf2431..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import 'angular-mocks'; -import $ from 'jquery'; - -import sinon from 'sinon'; - -import { initAngularBootstrap } from '../../../../../kibana_legacy/public'; -import { FixedScrollProvider } from './fixed_scroll'; - -const testModuleName = 'fixedScroll'; - -angular.module(testModuleName, []).directive('fixedScroll', FixedScrollProvider); - -describe('FixedScroll directive', function () { - const sandbox = sinon.createSandbox(); - let mockWidth; - let mockHeight; - let currentWidth = 120; - let currentHeight = 120; - let currentJqLiteWidth = 120; - let spyScrollWidth; - - let compile; - let flushPendingTasks; - const trash = []; - - beforeAll(() => { - mockWidth = jest.spyOn($.prototype, 'width').mockImplementation(function (width) { - if (width === undefined) { - return currentWidth; - } else { - currentWidth = width; - return this; - } - }); - mockHeight = jest.spyOn($.prototype, 'height').mockImplementation(function (height) { - if (height === undefined) { - return currentHeight; - } else { - currentHeight = height; - return this; - } - }); - angular.element.prototype.width = jest.fn(function (width) { - if (width === undefined) { - return currentJqLiteWidth; - } else { - currentJqLiteWidth = width; - return this; - } - }); - angular.element.prototype.offset = jest.fn(() => ({ top: 0 })); - }); - - beforeEach(() => { - currentJqLiteWidth = 120; - initAngularBootstrap(); - - angular.mock.module(testModuleName); - angular.mock.inject(($compile, $rootScope, $timeout) => { - flushPendingTasks = function flushPendingTasks() { - $rootScope.$digest(); - $timeout.flush(); - }; - - compile = function (ratioY, ratioX) { - if (ratioX == null) ratioX = ratioY; - - // since the directive works at the sibling level we create a - // parent for everything to happen in - const $parent = $('<div>').css({ - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - }); - - $parent.appendTo(document.body); - trash.push($parent); - - const $el = $('<div fixed-scroll></div>') - .css({ - 'overflow-x': 'auto', - width: $parent.width(), - }) - .appendTo($parent); - - spyScrollWidth = jest.spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get'); - spyScrollWidth.mockReturnValue($parent.width() * ratioX); - angular.element.prototype.height = jest.fn(() => $parent.height() * ratioY); - - const $content = $('<div>') - .css({ - width: $parent.width() * ratioX, - height: $parent.height() * ratioY, - }) - .appendTo($el); - - $compile($parent)($rootScope); - flushPendingTasks(); - - return { - $container: $el, - $content: $content, - $scroller: $parent.find('.dscTableFixedScroll__scroller'), - }; - }; - }); - }); - - afterEach(function () { - trash.splice(0).forEach(function ($el) { - $el.remove(); - }); - - sandbox.restore(); - spyScrollWidth.mockRestore(); - }); - - afterAll(() => { - mockWidth.mockRestore(); - mockHeight.mockRestore(); - delete angular.element.prototype.width; - delete angular.element.prototype.height; - delete angular.element.prototype.offset; - }); - - test('does nothing when not needed', function () { - let els = compile(0.5, 1.5); - expect(els.$scroller).toHaveLength(0); - - els = compile(1.5, 0.5); - expect(els.$scroller).toHaveLength(0); - }); - - test('attaches a scroller below the element when the content is larger then the container', function () { - const els = compile(1.5); - expect(els.$scroller.length).toBe(1); - }); - - test('copies the width of the container', function () { - const els = compile(1.5); - expect(els.$scroller.width()).toBe(els.$container.width()); - }); - - test('mimics the scrollWidth of the element', function () { - const els = compile(1.5); - expect(els.$scroller.prop('scrollWidth')).toBe(els.$container.prop('scrollWidth')); - }); - - describe('scroll event handling / tug of war prevention', function () { - test('listens when needed, unlistens when not needed', function (done) { - const on = sandbox.spy($.fn, 'on'); - const off = sandbox.spy($.fn, 'off'); - const jqLiteOn = sandbox.spy(angular.element.prototype, 'on'); - const jqLiteOff = sandbox.spy(angular.element.prototype, 'off'); - - const els = compile(1.5); - expect(on.callCount).toBe(1); - expect(jqLiteOn.callCount).toBe(1); - checkThisVals('$.fn.on', on, jqLiteOn); - - expect(off.callCount).toBe(0); - expect(jqLiteOff.callCount).toBe(0); - currentJqLiteWidth = els.$container.prop('scrollWidth'); - flushPendingTasks(); - expect(off.callCount).toBe(1); - expect(jqLiteOff.callCount).toBe(1); - checkThisVals('$.fn.off', off, jqLiteOff); - done(); - - function checkThisVals(namejQueryFn, spyjQueryFn, spyjqLiteFn) { - // the this values should be different - expect(spyjQueryFn.thisValues[0].is(spyjqLiteFn.thisValues[0])).toBeFalsy(); - // but they should be either $scroller or $container - const el = spyjQueryFn.thisValues[0]; - - if (el.is(els.$scroller) || el.is(els.$container)) return; - - done.fail('expected ' + namejQueryFn + ' to be called with $scroller or $container'); - } - }); - - // Turn off this row because tests failed. - // Scroll event is not catched in fixed_scroll. - // As container is jquery element in test but inside fixed_scroll it's a jqLite element. - // it would need jquery in jest to make this work. - [ - //{ from: '$container', to: '$scroller' }, - { from: '$scroller', to: '$container' }, - ].forEach(function (names) { - describe('scroll events ' + JSON.stringify(names), function () { - let spyJQueryScrollLeft; - let spyJQLiteScrollLeft; - let els; - let $from; - let $to; - - beforeEach(function () { - spyJQueryScrollLeft = sandbox.spy($.fn, 'scrollLeft'); - spyJQLiteScrollLeft = sandbox.stub(); - angular.element.prototype.scrollLeft = spyJQLiteScrollLeft; - els = compile(1.5); - $from = els[names.from]; - $to = els[names.to]; - }); - - afterAll(() => { - delete angular.element.prototype.scrollLeft; - }); - - test('transfers the scrollLeft', function () { - expect(spyJQueryScrollLeft.callCount).toBe(0); - expect(spyJQLiteScrollLeft.callCount).toBe(0); - $from.scroll(); - expect(spyJQueryScrollLeft.callCount).toBe(1); - expect(spyJQLiteScrollLeft.callCount).toBe(1); - - // first call should read the scrollLeft from the $container - const firstCall = spyJQueryScrollLeft.getCall(0); - expect(firstCall.args).toEqual([]); - - // second call should be setting the scrollLeft on the $scroller - const secondCall = spyJQLiteScrollLeft.getCall(0); - expect(secondCall.args).toEqual([firstCall.returnValue]); - }); - - /** - * In practice, calling $el.scrollLeft() causes the "scroll" event to trigger, - * but the browser seems to be very careful about triggering the event too much - * and I can't reliably recreate the browsers behavior in a test. So... faking it! - */ - test('prevents tug of war by ignoring echo scroll events', function () { - $from.scroll(); - expect(spyJQueryScrollLeft.callCount).toBe(1); - expect(spyJQLiteScrollLeft.callCount).toBe(1); - - spyJQueryScrollLeft.resetHistory(); - spyJQLiteScrollLeft.resetHistory(); - $to.scroll(); - expect(spyJQueryScrollLeft.callCount).toBe(0); - expect(spyJQLiteScrollLeft.callCount).toBe(0); - }); - }); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/directives/uninitialized.tsx b/src/plugins/discover/public/application/angular/directives/uninitialized.tsx index d04aea0933115..f2b1f584224ef 100644 --- a/src/plugins/discover/public/application/angular/directives/uninitialized.tsx +++ b/src/plugins/discover/public/application/angular/directives/uninitialized.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiButton, EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; interface Props { onRefresh: () => void; @@ -29,39 +29,30 @@ interface Props { export const DiscoverUninitialized = ({ onRefresh }: Props) => { return ( <I18nProvider> - <EuiPage> - <EuiPageBody> - <EuiPageContent horizontalPosition="center"> - <EuiEmptyPrompt - iconType="discoverApp" - title={ - <h2> - <FormattedMessage - id="discover.uninitializedTitle" - defaultMessage="Start searching" - /> - </h2> - } - body={ - <p> - <FormattedMessage - id="discover.uninitializedText" - defaultMessage="Write a query, add some filters, or simply hit Refresh to retrieve results for the current query." - /> - </p> - } - actions={ - <EuiButton color="primary" fill onClick={onRefresh}> - <FormattedMessage - id="discover.uninitializedRefreshButtonText" - defaultMessage="Refresh data" - /> - </EuiButton> - } + <EuiEmptyPrompt + iconType="discoverApp" + title={ + <h2> + <FormattedMessage id="discover.uninitializedTitle" defaultMessage="Start searching" /> + </h2> + } + body={ + <p> + <FormattedMessage + id="discover.uninitializedText" + defaultMessage="Write a query, add some filters, or simply hit Refresh to retrieve results for the current query." /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> + </p> + } + actions={ + <EuiButton color="primary" fill onClick={onRefresh}> + <FormattedMessage + id="discover.uninitializedRefreshButtonText" + defaultMessage="Refresh data" + /> + </EuiButton> + } + /> </I18nProvider> ); }; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index d0340c2cf4edd..2c3b8fd9606a9 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -33,7 +33,6 @@ import { syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; -import { createFixedScroll } from './directives/fixed_scroll'; import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; @@ -181,7 +180,7 @@ app.directive('discoverApp', function () { }; }); -function discoverController($element, $route, $scope, $timeout, $window, Promise, uiCapabilities) { +function discoverController($element, $route, $scope, $timeout, Promise, uiCapabilities) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); @@ -434,7 +433,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise savedSearch: savedSearch, indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, - fixedScroll: createFixedScroll($scope, $timeout), setHeaderActionMenu: getHeaderActionMenuMounter(), data, }; diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts index 1d38d0fc534d1..f7f7d4dd90eaf 100644 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts @@ -30,19 +30,26 @@ export function createInfiniteScrollDirective() { more: '=', }, link: ($scope: LazyScope, $element: JQuery) => { - const $window = $(window); let checkTimer: any; + /** + * depending on which version of Discover is displayed, different elements are scrolling + * and have therefore to be considered for calculation of infinite scrolling + */ + const scrollDiv = $element.parents('.dscTable'); + const scrollDivMobile = $(window); function onScroll() { if (!$scope.more) return; + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; + const scrollTop = usedScrollDiv.scrollTop(); - const winHeight = Number($window.height()); - const winBottom = Number(winHeight) + Number($window.scrollTop()); - const offset = $element.offset(); - const elTop = offset ? offset.top : 0; + const winHeight = Number(usedScrollDiv.height()); + const winBottom = Number(winHeight) + Number(scrollTop); + const elTop = $element.get(0).offsetTop || 0; const remaining = elTop - winBottom; - if (remaining <= winHeight * 0.5) { + if (remaining <= winHeight) { $scope[$scope.$$phase ? '$eval' : '$apply'](function () { $scope.more(); }); @@ -57,10 +64,12 @@ export function createInfiniteScrollDirective() { }, 50); } - $window.on('scroll', scheduleCheck); + scrollDiv.on('scroll', scheduleCheck); + window.addEventListener('scroll', scheduleCheck); $scope.$on('$destroy', function () { clearTimeout(checkTimer); - $window.off('scroll', scheduleCheck); + scrollDiv.off('scroll', scheduleCheck); + window.removeEventListener('scroll', scheduleCheck); }); scheduleCheck(); }, diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts index 73ae691529e2b..2605ec5bf6745 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts @@ -76,6 +76,12 @@ export function getSort(sort: SortPair[] | SortPair, indexPattern: IndexPattern) * compared to getSort it doesn't return an array of objects, it returns an array of arrays * [[fieldToSort: directionToSort]] */ -export function getSortArray(sort: SortPair[], indexPattern: IndexPattern) { - return getSort(sort, indexPattern).map((sortPair) => Object.entries(sortPair).pop()); +export function getSortArray(sort: SortPair[], indexPattern: IndexPattern): SortPairArr[] { + return getSort(sort, indexPattern).reduce((acc: SortPairArr[], sortPair) => { + const entries = Object.entries(sortPair); + if (entries && entries[0]) { + acc.push(entries[0]); + } + return acc; + }, []); } diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss deleted file mode 100644 index 87194d834827b..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss +++ /dev/null @@ -1,5 +0,0 @@ -.dscCxtAppContent { - border: none; - background-color: transparent; - box-shadow: none; -} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index b5387ec51db81..af99c995c60eb 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import './context_app_legacy.scss'; import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiPanel, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, @@ -100,14 +99,9 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { const loadingFeedback = () => { if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { return ( - <EuiPanel paddingSize="l" data-test-subj="contextApp_loadingIndicator"> - <EuiText textAlign="center"> - <FormattedMessage - id="discover.context.loadingDescription" - defaultMessage="Loading..." - /> - </EuiText> - </EuiPanel> + <EuiText textAlign="center" data-test-subj="contextApp_loadingIndicator"> + <FormattedMessage id="discover.context.loadingDescription" defaultMessage="Loading..." /> + </EuiText> ); } return null; @@ -122,13 +116,13 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { <EuiPageContent paddingSize="s" className="dscCxtAppContent"> <ActionBar {...actionBarProps(PREDECESSOR_TYPE)} /> {loadingFeedback()} + <EuiHorizontalRule margin="xs" /> {isLoaded ? ( - <EuiPanel paddingSize="none"> - <div className="discover-table"> - <DocTableLegacy {...docTableProps()} /> - </div> - </EuiPanel> + <div className="discover-table"> + <DocTableLegacy {...docTableProps()} /> + </div> ) : null} + <EuiHorizontalRule margin="xs" /> <ActionBar {...actionBarProps(SUCCESSOR_TYPE)} /> </EuiPageContent> </EuiPage> diff --git a/src/plugins/discover/public/application/components/discover.scss b/src/plugins/discover/public/application/components/discover.scss new file mode 100644 index 0000000000000..b17da97a45930 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover.scss @@ -0,0 +1,91 @@ +discover-app { + flex-grow: 1; +} + +.dscPage { + @include euiBreakpoint('m', 'l', 'xl') { + height: calc(100vh - #{($euiHeaderHeightCompensation * 2)}); + } + + flex-direction: column; + overflow: hidden; + padding: 0; + + .dscPageBody { + overflow: hidden; + } +} + +.dscPageBody__inner { + overflow: hidden; + height: 100%; +} + +.dscPageBody__contents { + overflow: hidden; + padding-top: $euiSizeXS / 2; // A little breathing room for the index pattern button +} + +.dscPageContent__wrapper { + padding: 0 $euiSize $euiSize 0; + overflow: hidden; // Ensures horizontal scroll of table + + @include euiBreakpoint('xs', 's') { + padding: 0 $euiSize $euiSize; + } +} + +.dscPageContent, +.dscPageContent__inner { + height: 100%; +} + +.dscPageContent--centered { + height: auto; +} + +.dscResultCount { + padding: $euiSizeS; + + @include euiBreakpoint('xs', 's') { + .dscResultCount__toggle { + align-items: flex-end; + } + + .dscResuntCount__title, + .dscResultCount__actions { + margin-bottom: 0 !important; + } + } +} + +.dscTimechart { + display: block; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } +} + +.dscHistogram { + display: flex; + height: $euiSize * 12.5; + padding: $euiSizeS; +} + +.dscTable { + // SASSTODO: add a monospace modifier to the doc-table component + .kbnDocTable__row { + font-family: $euiCodeFontFamily; + font-size: $euiFontSizeXS; + } +} + +.dscTable__footer { + background-color: $euiColorLightShade; + padding: $euiSizeXS $euiSizeS; + text-align: center; +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index e9de4c08a177b..56f8fa46a9f69 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -16,23 +16,32 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState, useCallback, useEffect } from 'react'; -import classNames from 'classnames'; -import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import './discover.scss'; + +import React, { useState, useRef } from 'react'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHideFor, + EuiPage, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient, MountPoint } from 'kibana/public'; +import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; -import { DiscoverSidebar } from './sidebar'; import { getServices, IndexPattern } from '../../kibana_services'; import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; +import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; import { - IndexPatternField, search, ISearchSource, TimeRange, @@ -40,15 +49,20 @@ import { IndexPatternAttributes, DataPublicPluginStart, AggConfigs, + FilterManager, } from '../../../../data/public'; import { Chart } from '../angular/helpers/point_series'; import { AppState } from '../angular/discover_state'; import { SavedSearch } from '../../saved_searches'; - import { SavedObject } from '../../../../../core/types'; import { TopNavMenuData } from '../../../../navigation/public'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './sidebar/discover_sidebar_responsive'; +import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; -export interface DiscoverLegacyProps { +export interface DiscoverProps { addColumn: (column: string) => void; fetch: () => void; fetchCounter: number; @@ -58,7 +72,7 @@ export interface DiscoverLegacyProps { hits: number; indexPattern: IndexPattern; minimumVisibleRows: number; - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + onAddFilter: DocViewFilterFn; onChangeInterval: (interval: string) => void; onMoveColumn: (columns: string, newIdx: number) => void; onRemoveColumn: (column: string) => void; @@ -70,15 +84,17 @@ export interface DiscoverLegacyProps { config: IUiSettingsClient; data: DataPublicPluginStart; fixedScroll: (el: HTMLElement) => void; + filterManager: FilterManager; indexPatternList: Array<SavedObject<IndexPatternAttributes>>; sampleSize: number; savedSearch: SavedSearch; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; timefield: string; + setAppState: (state: Partial<AppState>) => void; }; resetQuery: () => void; resultState: string; - rows: Array<Record<string, unknown>>; + rows: ElasticSearchHit[]; searchSource: ISearchSource; setIndexPattern: (id: string) => void; showSaveQuery: boolean; @@ -90,6 +106,13 @@ export interface DiscoverLegacyProps { updateSavedQueryId: (savedQueryId?: string) => void; } +export const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( + <DocTableLegacy {...props} /> +)); +export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( + <DiscoverSidebarResponsive {...props} /> +)); + export function DiscoverLegacy({ addColumn, fetch, @@ -119,43 +142,30 @@ export function DiscoverLegacy({ topNavMenu, updateQuery, updateSavedQueryId, -}: DiscoverLegacyProps) { +}: DiscoverProps) { + const scrollableDesktop = useRef<HTMLDivElement>(null); + const collapseIcon = useRef<HTMLButtonElement>(null); + const isMobile = () => { + // collapse icon isn't displayed in mobile view, use it to detect which view is displayed + return collapseIcon && !collapseIcon.current; + }; + + const [toggleOn, toggleChart] = useState(true); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const { TopNavMenu } = getServices().navigation.ui; - const { trackUiMetric } = getServices(); + const services = getServices(); + const { TopNavMenu } = services.navigation.ui; + const { trackUiMetric } = services; const { savedSearch, indexPatternList } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) ? bucketAggConfig.buckets?.getInterval() : undefined; - const [fixedScrollEl, setFixedScrollEl] = useState<HTMLElement | undefined>(); - - useEffect(() => (fixedScrollEl ? opts.fixedScroll(fixedScrollEl) : undefined), [ - fixedScrollEl, - opts, - ]); - const fixedScrollRef = useCallback( - (node: HTMLElement) => { - if (node !== null) { - setFixedScrollEl(node); - } - }, - [setFixedScrollEl] - ); - const sidebarClassName = classNames({ - closed: isSidebarClosed, - }); - - const mainSectionClassName = classNames({ - 'col-md-10': !isSidebarClosed, - 'col-md-12': isSidebarClosed, - }); + const contentCentered = resultState === 'uninitialized'; return ( <I18nProvider> - <div className="dscAppContainer" data-fetch-counter={fetchCounter}> - <h1 className="euiScreenReaderOnly">{savedSearch.title}</h1> + <EuiPage className="dscPage" data-fetch-counter={fetchCounter}> <TopNavMenu appName="discover" config={topNavMenu} @@ -171,150 +181,212 @@ export function DiscoverLegacy({ showSearchBar={true} useDefaultBehaviors={true} /> - <main className="container-fluid"> - <div className="row"> - <div - className={`col-md-2 dscSidebar__container dscCollapsibleSidebar ${sidebarClassName}`} - id="discover-sidebar" - data-test-subj="discover-sidebar" - > - {!isSidebarClosed && ( - <div className="dscFieldChooser"> - <DiscoverSidebar - columns={state.columns || []} - fieldCounts={fieldCounts} - hits={rows} - indexPatternList={indexPatternList} - onAddField={addColumn} - onAddFilter={onAddFilter} - onRemoveField={onRemoveColumn} - selectedIndexPattern={searchSource && searchSource.getField('index')} - setIndexPattern={setIndexPattern} - trackUiMetric={trackUiMetric} - /> - </div> - )} - <EuiButtonIcon - iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'} - iconSize="m" - size="s" - onClick={() => setIsSidebarClosed(!isSidebarClosed)} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label="Toggle sidebar" - className="dscCollapsibleSidebar__collapseButton" + <EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle"> + <h1 id="savedSearchTitle" className="euiScreenReaderOnly"> + {savedSearch.title} + </h1> + <EuiFlexGroup className="dscPageBody__contents" gutterSize="none"> + <EuiFlexItem grow={false}> + <SidebarMemoized + columns={state.columns || []} + fieldCounts={fieldCounts} + hits={rows} + indexPatternList={indexPatternList} + onAddField={addColumn} + onAddFilter={onAddFilter} + onRemoveField={onRemoveColumn} + selectedIndexPattern={searchSource && searchSource.getField('index')} + services={services} + setIndexPattern={setIndexPattern} + isClosed={isSidebarClosed} + trackUiMetric={trackUiMetric} /> - </div> - <div className={`dscWrapper ${mainSectionClassName}`}> - {resultState === 'none' && ( - <DiscoverNoResults - timeFieldName={opts.timefield} - queryLanguage={state.query ? state.query.language : ''} - data={opts.data} - error={fetchError} + </EuiFlexItem> + <EuiHideFor sizes={['xs', 's']}> + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'} + onClick={() => setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label="Toggle sidebar" + buttonRef={collapseIcon} /> - )} - {resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />} - {resultState === 'loading' && <LoadingSpinner />} - {resultState === 'ready' && ( - <div className="dscWrapper__content"> - <SkipBottomButton onClick={onSkipBottomButtonClick} /> - <HitsCounter - hits={hits > 0 ? hits : 0} - showResetButton={!!(savedSearch && savedSearch.id)} - onResetQuery={resetQuery} + </EuiFlexItem> + </EuiHideFor> + <EuiFlexItem className="dscPageContent__wrapper"> + <EuiPageContent + verticalPosition={contentCentered ? 'center' : undefined} + horizontalPosition={contentCentered ? 'center' : undefined} + paddingSize="none" + className={classNames('dscPageContent', { + 'dscPageContent--centered': contentCentered, + })} + > + {resultState === 'none' && ( + <DiscoverNoResults + timeFieldName={opts.timefield} + queryLanguage={state.query ? state.query.language : ''} + data={opts.data} + error={fetchError} /> - {opts.timefield && ( - <TimechartHeader - dateFormat={opts.config.get('dateFormat')} - timeRange={timeRange} - options={search.aggs.intervalOptions} - onChangeInterval={onChangeInterval} - stateInterval={state.interval || ''} - bucketInterval={bucketInterval} - /> - )} - - {opts.timefield && ( - <section - aria-label={i18n.translate('discover.histogramOfFoundDocumentsAriaLabel', { - defaultMessage: 'Histogram of found documents', - })} - className="dscTimechart" - > - {opts.chartAggConfigs && rows.length !== 0 && ( - <div className="dscHistogram" data-test-subj="discoverChart"> - <DiscoverHistogram - chartData={histogramData} - timefilterUpdateHandler={timefilterUpdateHandler} - /> - </div> - )} - </section> - )} - - <div className="dscResults"> - <section - className="dscTable dscTableFixedScroll" - aria-labelledby="documentsAriaLabel" - ref={fixedScrollRef} - > - <h2 className="euiScreenReaderOnly" id="documentsAriaLabel"> - <FormattedMessage - id="discover.documentsAriaLabel" - defaultMessage="Documents" - /> - </h2> - {rows && rows.length && ( - <div className="dscDiscover"> - <DocTableLegacy - columns={state.columns || []} - indexPattern={indexPattern} - minimumVisibleRows={minimumVisibleRows} - rows={rows} - sort={state.sort || []} - searchDescription={opts.savedSearch.description} - searchTitle={opts.savedSearch.lastSavedTitle} - onAddColumn={addColumn} - onFilter={onAddFilter} - onMoveColumn={onMoveColumn} - onRemoveColumn={onRemoveColumn} - onSort={onSort} + )} + {resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />} + {resultState === 'loading' && <LoadingSpinner />} + {resultState === 'ready' && ( + <EuiFlexGroup + className="dscPageContent__inner" + direction="column" + alignItems="stretch" + gutterSize="none" + responsive={false} + > + <EuiFlexItem grow={false} className="dscResultCount"> + <EuiFlexGroup alignItems="center" justifyContent="spaceBetween"> + <EuiFlexItem + grow={false} + className="dscResuntCount__title eui-textTruncate eui-textNoWrap" + > + <HitsCounter + hits={hits > 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} /> - <a tabIndex={0} id="discoverBottomMarker"> - ​ - </a> - {rows.length === opts.sampleSize && ( - <div - className="dscTable__footer" - data-test-subj="discoverDocTableFooter" - > - <FormattedMessage - id="discover.howToSeeOtherMatchingDocumentsDescription" - defaultMessage="These are the first {sampleSize} documents matching - your search, refine your search to see others." - values={{ sampleSize: opts.sampleSize }} + </EuiFlexItem> + {toggleOn && ( + <EuiFlexItem className="dscResultCount__actions"> + <TimechartHeader + dateFormat={opts.config.get('dateFormat')} + timeRange={timeRange} + options={search.aggs.intervalOptions} + onChangeInterval={onChangeInterval} + stateInterval={state.interval || ''} + bucketInterval={bucketInterval} + /> + </EuiFlexItem> + )} + <EuiFlexItem className="dscResultCount__toggle" grow={false}> + <EuiButtonEmpty + size="xs" + iconType={toggleOn ? 'eyeClosed' : 'eye'} + onClick={() => { + toggleChart(!toggleOn); + }} + > + {toggleOn + ? i18n.translate('discover.hideChart', { + defaultMessage: 'Hide chart', + }) + : i18n.translate('discover.showChart', { + defaultMessage: 'Show chart', + })} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <SkipBottomButton onClick={onSkipBottomButtonClick} /> + </EuiFlexItem> + {toggleOn && opts.timefield && ( + <EuiFlexItem grow={false}> + <section + aria-label={i18n.translate( + 'discover.histogramOfFoundDocumentsAriaLabel', + { + defaultMessage: 'Histogram of found documents', + } + )} + className="dscTimechart" + > + {opts.chartAggConfigs && rows.length !== 0 && ( + <div className="dscHistogram" data-test-subj="discoverChart"> + <DiscoverHistogram + chartData={histogramData} + timefilterUpdateHandler={timefilterUpdateHandler} /> + </div> + )} + </section> + </EuiFlexItem> + )} - <EuiButtonEmpty onClick={() => window.scrollTo(0, 0)}> + <EuiFlexItem className="eui-yScroll"> + <section + className="dscTable eui-yScroll" + aria-labelledby="documentsAriaLabel" + ref={scrollableDesktop} + tabIndex={-1} + > + <h2 className="euiScreenReaderOnly" id="documentsAriaLabel"> + <FormattedMessage + id="discover.documentsAriaLabel" + defaultMessage="Documents" + /> + </h2> + {rows && rows.length && ( + <div> + <DocTableLegacyMemoized + columns={state.columns || []} + indexPattern={indexPattern} + minimumVisibleRows={minimumVisibleRows} + rows={rows} + sort={state.sort || []} + searchDescription={opts.savedSearch.description} + searchTitle={opts.savedSearch.lastSavedTitle} + onAddColumn={addColumn} + onFilter={onAddFilter} + onMoveColumn={onMoveColumn} + onRemoveColumn={onRemoveColumn} + onSort={onSort} + /> + {rows.length === opts.sampleSize ? ( + <div + className="dscTable__footer" + data-test-subj="discoverDocTableFooter" + tabIndex={-1} + id="discoverBottomMarker" + > <FormattedMessage - id="discover.backToTopLinkText" - defaultMessage="Back to top." + id="discover.howToSeeOtherMatchingDocumentsDescription" + defaultMessage="These are the first {sampleSize} documents matching + your search, refine your search to see others." + values={{ sampleSize: opts.sampleSize }} /> - </EuiButtonEmpty> - </div> - )} - </div> - )} - </section> - </div> - </div> - )} - </div> - </div> - </main> - </div> + + <EuiButtonEmpty + onClick={() => { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }} + > + <FormattedMessage + id="discover.backToTopLinkText" + defaultMessage="Back to top." + /> + </EuiButtonEmpty> + </div> + ) : ( + <span tabIndex={-1} id="discoverBottomMarker"> + ​ + </span> + )} + </div> + )} + </section> + </EuiFlexItem> + </EuiFlexGroup> + )} + </EuiPageContent> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPageBody> + </EuiPage> </I18nProvider> ); } diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx index 2623b5a270a31..d43a09bd51c6a 100644 --- a/src/plugins/discover/public/application/components/doc/doc.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; +import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; import { getServices } from '../../../kibana_services'; @@ -49,84 +49,86 @@ export function Doc(props: DocProps) { return ( <I18nProvider> - <EuiPageContent> - {reqState === ElasticRequestState.NotFoundIndexPattern && ( - <EuiCallOut - color="danger" - data-test-subj={`doc-msg-notFoundIndexPattern`} - iconType="alert" - title={ - <FormattedMessage - id="discover.doc.failedToLocateIndexPattern" - defaultMessage="No index pattern matches ID {indexPatternId}" - values={{ indexPatternId: props.indexPatternId }} - /> - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - <EuiCallOut - color="danger" - data-test-subj={`doc-msg-notFound`} - iconType="alert" - title={ - <FormattedMessage - id="discover.doc.failedToLocateDocumentDescription" - defaultMessage="Cannot find document" - /> - } - > - <FormattedMessage - id="discover.doc.couldNotFindDocumentsDescription" - defaultMessage="No documents match that ID." + <EuiPage> + <EuiPageContent> + {reqState === ElasticRequestState.NotFoundIndexPattern && ( + <EuiCallOut + color="danger" + data-test-subj={`doc-msg-notFoundIndexPattern`} + iconType="alert" + title={ + <FormattedMessage + id="discover.doc.failedToLocateIndexPattern" + defaultMessage="No index pattern matches ID {indexPatternId}." + values={{ indexPatternId: props.indexPatternId }} + /> + } /> - </EuiCallOut> - )} - - {reqState === ElasticRequestState.Error && ( - <EuiCallOut - color="danger" - data-test-subj={`doc-msg-error`} - iconType="alert" - title={ + )} + {reqState === ElasticRequestState.NotFound && ( + <EuiCallOut + color="danger" + data-test-subj={`doc-msg-notFound`} + iconType="alert" + title={ + <FormattedMessage + id="discover.doc.failedToLocateDocumentDescription" + defaultMessage="Cannot find document" + /> + } + > <FormattedMessage - id="discover.doc.failedToExecuteQueryDescription" - defaultMessage="Cannot run search" + id="discover.doc.couldNotFindDocumentsDescription" + defaultMessage="No documents match that ID." /> - } - > - <FormattedMessage - id="discover.doc.somethingWentWrongDescription" - defaultMessage="{indexName} is missing." - values={{ indexName: props.index }} - />{' '} - <EuiLink - href={`https://www.elastic.co/guide/en/elasticsearch/reference/${ - getServices().metadata.branch - }/indices-exists.html`} - target="_blank" + </EuiCallOut> + )} + + {reqState === ElasticRequestState.Error && ( + <EuiCallOut + color="danger" + data-test-subj={`doc-msg-error`} + iconType="alert" + title={ + <FormattedMessage + id="discover.doc.failedToExecuteQueryDescription" + defaultMessage="Cannot run search" + /> + } > <FormattedMessage - id="discover.doc.somethingWentWrongDescriptionAddon" - defaultMessage="Please ensure the index exists." - /> - </EuiLink> - </EuiCallOut> - )} + id="discover.doc.somethingWentWrongDescription" + defaultMessage="{indexName} is missing." + values={{ indexName: props.index }} + />{' '} + <EuiLink + href={`https://www.elastic.co/guide/en/elasticsearch/reference/${ + getServices().metadata.branch + }/indices-exists.html`} + target="_blank" + > + <FormattedMessage + id="discover.doc.somethingWentWrongDescriptionAddon" + defaultMessage="Please ensure the index exists." + /> + </EuiLink> + </EuiCallOut> + )} - {reqState === ElasticRequestState.Loading && ( - <EuiCallOut data-test-subj={`doc-msg-loading`}> - <EuiLoadingSpinner size="m" />{' '} - <FormattedMessage id="discover.doc.loadingDescription" defaultMessage="Loading…" /> - </EuiCallOut> - )} + {reqState === ElasticRequestState.Loading && ( + <EuiCallOut data-test-subj={`doc-msg-loading`}> + <EuiLoadingSpinner size="m" />{' '} + <FormattedMessage id="discover.doc.loadingDescription" defaultMessage="Loading…" /> + </EuiCallOut> + )} - {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( - <div data-test-subj="doc-hit"> - <DocViewer hit={hit} indexPattern={indexPattern} /> - </div> - )} - </EuiPageContent> + {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( + <div data-test-subj="doc-hit"> + <DocViewer hit={hit} indexPattern={indexPattern} /> + </div> + )} + </EuiPageContent> + </EuiPage> </I18nProvider> ); } diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index ec2beca15a546..b6b7a244bd1f6 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -1,5 +1,8 @@ .kbnDocViewerTable { margin-top: $euiSizeS; + @include euiBreakpoint('xs', 's') { + table-layout: fixed; + } } .kbnDocViewer { @@ -11,10 +14,10 @@ white-space: pre-wrap; color: $euiColorFullShade; vertical-align: top; - padding-top: 2px; + padding-top: $euiSizeXS * 0.5; } .kbnDocViewer__field { - padding-top: 8px; + padding-top: $euiSizeS; } .dscFieldName { @@ -42,10 +45,9 @@ white-space: nowrap; } .kbnDocViewer__buttons { - width: 60px; + width: 96px; // Show all icons if one is focused, - // IE doesn't support, but the fallback is just the focused button becomes visible &:focus-within { .kbnDocViewer__actionButton { opacity: 1; @@ -54,11 +56,16 @@ } .kbnDocViewer__field { - width: 160px; + width: $euiSize * 10; + @include euiBreakpoint('xs', 's') { + width: $euiSize * 6; + } } .kbnDocViewer__actionButton { - opacity: 0; + @include euiBreakpoint('m', 'l', 'xl') { + opacity: 0; + } &:focus { opacity: 1; @@ -68,4 +75,3 @@ .kbnDocViewer__warning { margin-right: $euiSizeS; } - diff --git a/src/plugins/discover/public/application/components/field_name/field_name.tsx b/src/plugins/discover/public/application/components/field_name/field_name.tsx index b8f664d6cf38a..049557dbe1971 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.tsx @@ -30,6 +30,7 @@ interface Props { fieldMapping?: FieldMapping; fieldIconProps?: Omit<FieldIconProps, 'type'>; scripted?: boolean; + className?: string; } export function FieldName({ @@ -37,6 +38,7 @@ export function FieldName({ fieldMapping, fieldType, fieldIconProps, + className, scripted = false, }: Props) { const typeName = getFieldTypeName(fieldType); @@ -45,7 +47,7 @@ export function FieldName({ const tooltip = displayName !== fieldName ? `${fieldName} (${displayName})` : fieldName; return ( - <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}> + <EuiFlexGroup className={className} alignItems="center" gutterSize="s" responsive={false}> <EuiFlexItem grow={false}> <FieldIcon type={fieldType} label={typeName} scripted={scripted} {...fieldIconProps} /> </EuiFlexItem> diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss b/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss new file mode 100644 index 0000000000000..5a3999f129bf4 --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss @@ -0,0 +1,3 @@ +.dscHitsCounter { + flex-grow: 0; +} diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx index 1d2cd12877b1c..dfd155c3329e4 100644 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import './hits_counter.scss'; + import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; @@ -41,8 +43,8 @@ export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounter return ( <I18nProvider> <EuiFlexGroup + className="dscHitsCounter" gutterSize="s" - className="dscResultCount" responsive={false} justifyContent="center" alignItems="center" diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss new file mode 100644 index 0000000000000..a58897e43b615 --- /dev/null +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss @@ -0,0 +1,4 @@ +.dscLoading { + text-align: center; + padding: $euiSizeL 0; +} diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx index e3cc396783628..54aacb7870997 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx @@ -16,13 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import './loading_spinner.scss'; + import React from 'react'; import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export function LoadingSpinner() { return ( - <div className="dscOverlay"> + <div className="dscLoading"> <EuiTitle size="s" data-test-subj="loadingSpinnerText"> <h2> <FormattedMessage id="discover.searchingTitle" defaultMessage="Searching" /> diff --git a/src/plugins/discover/public/application/components/no_results/_no_results.scss b/src/plugins/discover/public/application/components/no_results/_no_results.scss index 7ea945e820bf9..6500593d57234 100644 --- a/src/plugins/discover/public/application/components/no_results/_no_results.scss +++ b/src/plugins/discover/public/application/components/no_results/_no_results.scss @@ -1,3 +1,3 @@ .dscNoResults { - max-width: 1000px; + padding: $euiSize; } diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx index fcc2912d16dd5..df28b4795b4fb 100644 --- a/src/plugins/discover/public/application/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -19,7 +19,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { getServices } from '../../../kibana_services'; import { DataPublicPluginStart } from '../../../../../data/public'; import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper'; @@ -85,7 +85,6 @@ export function DiscoverNoResults({ return ( <Fragment> - <EuiSpacer size="xl" /> <EuiFlexGroup justifyContent="center">{callOut}</EuiFlexGroup> </Fragment> ); diff --git a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx index e44c05b3a88a9..b997bd961ea7d 100644 --- a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx @@ -20,16 +20,16 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { - EuiButtonEmpty, + EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable, - EuiButtonEmptyProps, + EuiButtonProps, } from '@elastic/eui'; import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; import { IndexPatternRef } from './types'; -export type ChangeIndexPatternTriggerProps = EuiButtonEmptyProps & { +export type ChangeIndexPatternTriggerProps = EuiButtonProps & { label: string; title?: string; }; @@ -54,9 +54,8 @@ export function ChangeIndexPattern({ const createTrigger = function () { const { label, title, ...rest } = trigger; return ( - <EuiButtonEmpty - className="eui-textTruncate" - flush="left" + <EuiButton + fullWidth color="text" iconSide="right" iconType="arrowDown" @@ -64,8 +63,8 @@ export function ChangeIndexPattern({ onClick={() => setPopoverIsOpen(!isPopoverOpen)} {...rest} > - {label} - </EuiButtonEmpty> + <strong>{label}</strong> + </EuiButton> ); }; @@ -74,8 +73,6 @@ export function ChangeIndexPattern({ button={createTrigger()} isOpen={isPopoverOpen} closePopover={() => setPopoverIsOpen(false)} - className="eui-textTruncate" - anchorClassName="eui-textTruncate" display="block" panelPaddingSize="s" ownFocus diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 35515a6a0e7a5..cc55eaee54893 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -16,18 +16,24 @@ * specific language governing permissions and limitations * under the License. */ +import './discover_field.scss'; + import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiStatsMetricType } from '@kbn/analytics'; +import classNames from 'classnames'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; import { FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getFieldTypeName } from './lib/get_field_type_name'; -import './discover_field.scss'; export interface DiscoverFieldProps { + /** + * Determines whether add/remove button is displayed not only when focused + */ + alwaysShowActionButton?: boolean; /** * The displayed field */ @@ -66,6 +72,7 @@ export interface DiscoverFieldProps { } export function DiscoverField({ + alwaysShowActionButton = false, field, indexPattern, onAddField, @@ -120,7 +127,9 @@ export function DiscoverField({ {wrapOnDot(field.displayName)} </span> ); - + const actionBtnClassName = classNames('dscSidebarItem__action', { + ['dscSidebarItem__mobile']: alwaysShowActionButton, + }); let actionButton; if (field.name !== '_source' && !selected) { actionButton = ( @@ -132,7 +141,7 @@ export function DiscoverField({ > <EuiButtonIcon iconType="plusInCircleFilled" - className="dscSidebarItem__action" + className={actionBtnClassName} onClick={(ev: React.MouseEvent<HTMLButtonElement>) => { if (ev.type === 'click') { ev.currentTarget.focus(); @@ -157,7 +166,7 @@ export function DiscoverField({ <EuiButtonIcon color="danger" iconType="cross" - className="dscSidebarItem__action" + className={actionBtnClassName} onClick={(ev: React.MouseEvent<HTMLButtonElement>) => { if (ev.type === 'click') { ev.currentTarget.focus(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss b/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss index f4b3eed741f9f..ca48d67f75dec 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss @@ -1,3 +1,8 @@ +.dscFieldDetails { + color: $euiTextColor; + margin-bottom: $euiSizeS; +} + .dscFieldDetails__visualizeBtn { @include euiFontSizeXS; height: $euiSizeL !important; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss b/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss new file mode 100644 index 0000000000000..4b620f2073771 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss @@ -0,0 +1,7 @@ +.dscFieldSearch__formWrapper { + padding: $euiSizeM; +} + +.dscFieldSearch__filterWrapper { + width: 100%; +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 527be8cff9f0c..31928fd367951 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -50,17 +50,18 @@ describe('DiscoverFieldSearch', () => { test('change in active filters should change facet selection and call onChange', () => { const onChange = jest.fn(); const component = mountComponent({ ...defaultProps, ...{ onChange } }); - let btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy(); + const btn = findTestSubject(component, 'toggleFieldFilterButton'); + const badge = btn.find('.euiNotificationBadge'); + expect(badge.text()).toEqual('0'); btn.simulate('click'); const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); + act(() => { // @ts-ignore (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); component.update(); - btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true); + expect(badge.text()).toEqual('1'); expect(onChange).toBeCalledWith('aggregatable', true); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index a42e2412ae928..60eccefd35006 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -16,14 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import './discover_field_search.scss'; + import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiFacetButton, EuiFieldSearch, + EuiFilterGroup, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiPopover, EuiPopoverFooter, EuiPopoverTitle, @@ -34,6 +35,8 @@ import { EuiFormRow, EuiButtonGroup, EuiOutsideClickDetector, + EuiFilterButton, + EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -108,7 +111,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { defaultMessage: 'Show field filter settings', }); - const handleFacetButtonClicked = () => { + const handleFilterButtonClicked = () => { setPopoverOpen(!isPopoverOpen); }; @@ -162,20 +165,21 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { }; const buttonContent = ( - <EuiFacetButton + <EuiFilterButton aria-label={filterBtnAriaLabel} data-test-subj="toggleFieldFilterButton" - className="dscFieldSearch__toggleButton" - icon={<EuiIcon type="filter" />} + iconType="arrowDown" isSelected={activeFiltersCount > 0} - quantity={activeFiltersCount} - onClick={handleFacetButtonClicked} + numFilters={0} + hasActiveFilters={activeFiltersCount > 0} + numActiveFilters={activeFiltersCount} + onClick={handleFilterButtonClicked} > <FormattedMessage - id="discover.fieldChooser.fieldFilterFacetButtonLabel" + id="discover.fieldChooser.fieldFilterButtonLabel" defaultMessage="Filter by type" /> - </EuiFacetButton> + </EuiFilterButton> ); const select = ( @@ -255,7 +259,6 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { <EuiFieldSearch aria-label={searchPlaceholder} data-test-subj="fieldFilterSearchInput" - compressed fullWidth onChange={(event) => onChange('name', event.currentTarget.value)} placeholder={searchPlaceholder} @@ -263,13 +266,14 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { /> </EuiFlexItem> </EuiFlexGroup> - <div className="dscFieldSearch__filterWrapper"> - <EuiOutsideClickDetector onOutsideClick={() => {}} isDisabled={!isPopoverOpen}> + <EuiSpacer size="xs" /> + <EuiOutsideClickDetector onOutsideClick={() => {}} isDisabled={!isPopoverOpen}> + <EuiFilterGroup className="dscFieldSearch__filterWrapper"> <EuiPopover id="dataPanelTypeFilter" panelClassName="euiFilterGroup__popoverPanel" panelPaddingSize="none" - anchorPosition="downLeft" + anchorPosition="rightUp" display="block" isOpen={isPopoverOpen} closePopover={() => { @@ -294,8 +298,8 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { /> </EuiPopoverFooter> </EuiPopover> - </EuiOutsideClickDetector> - </div> + </EuiFilterGroup> + </EuiOutsideClickDetector> </React.Fragment> ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx index 3acdcb1e92091..0bb03492cfc75 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx @@ -65,26 +65,23 @@ export function DiscoverIndexPattern({ } return ( - <div className="dscIndexPattern__container"> - <I18nProvider> - <ChangeIndexPattern - trigger={{ - label: selected.title, - title: selected.title, - 'data-test-subj': 'indexPattern-switch-link', - className: 'dscIndexPattern__triggerButton', - }} - indexPatternId={selected.id} - indexPatternRefs={options} - onChangeIndexPattern={(id) => { - const indexPattern = options.find((pattern) => pattern.id === id); - if (indexPattern) { - setIndexPattern(id); - setSelected(indexPattern); - } - }} - /> - </I18nProvider> - </div> + <I18nProvider> + <ChangeIndexPattern + trigger={{ + label: selected.title, + title: selected.title, + 'data-test-subj': 'indexPattern-switch-link', + }} + indexPatternId={selected.id} + indexPatternRefs={options} + onChangeIndexPattern={(id) => { + const indexPattern = options.find((pattern) => pattern.id === id); + if (indexPattern) { + setIndexPattern(id); + setSelected(indexPattern); + } + }} + /> + </I18nProvider> ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index f130b0399f467..aaf1743653d7d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -1,26 +1,37 @@ -.dscSidebar__container { - padding-left: 0 !important; - padding-right: 0 !important; - background-color: transparent; - border-right-color: transparent; - border-bottom-color: transparent; +.dscSidebar { + margin: 0; + flex-grow: 1; + padding-left: $euiSize; + width: $euiSize * 19; + height: 100%; + + @include euiBreakpoint('xs', 's') { + width: 100%; + padding: $euiSize $euiSize 0 $euiSize; + background-color: $euiPageBackgroundColor; + } } -.dscIndexPattern__container { - display: flex; - align-items: center; - height: $euiSize * 3; - margin-top: -$euiSizeS; +.dscSidebar__group { + height: 100%; +} + +.dscSidebar__mobile { + width: 100%; + padding: $euiSize $euiSize 0; + + .dscSidebar__mobileBadge { + margin-left: $euiSizeS; + vertical-align: text-bottom; + } } -.dscIndexPattern__triggerButton { - @include euiTitle('xs'); - line-height: $euiSizeXXL; +.dscSidebar__flyoutHeader { + align-items: center; } .dscFieldList { - list-style: none; - margin-bottom: 0; + padding: 0 $euiSizeXS $euiSizeXS; } .dscFieldListHeader { @@ -29,18 +40,10 @@ } .dscFieldList--popular { + padding-bottom: $euiSizeS; background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); } -.dscFieldChooser { - padding-left: $euiSize; -} - -.dscFieldChooser__toggle { - color: $euiColorMediumShade; - margin-left: $euiSizeS !important; -} - .dscSidebarItem { &:hover, &:focus-within, @@ -57,40 +60,12 @@ */ .dscSidebarItem__action { opacity: 0; /* 1 */ - transition: none; + + &.dscSidebarItem__mobile { + opacity: 1; + } &:focus { opacity: 1; /* 2 */ } - font-size: $euiFontSizeXS; - padding: 2px 6px !important; - height: 22px !important; - min-width: auto !important; - .euiButton__content { - padding: 0 4px; - } -} - -.dscFieldSearch { - padding: $euiSizeS; -} - -.dscFieldSearch__toggleButton { - width: calc(100% - #{$euiSizeS}); - color: $euiColorPrimary; - padding-left: $euiSizeXS; - margin-left: $euiSizeXS; -} - -.dscFieldSearch__filterWrapper { - flex-grow: 0; -} - -.dscFieldSearch__formWrapper { - padding: $euiSizeM; -} - -.dscFieldDetails { - color: $euiTextColor; - margin-bottom: $euiSizeS; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 23d2fa0a39f34..74921a70e7f2f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore @@ -26,35 +26,41 @@ import realHits from 'fixtures/real_hits.js'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; +import { DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; +import { getDefaultFieldFilter } from './lib/field_filter'; +import { DiscoverSidebar } from './discover_sidebar'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -jest.mock('../../../kibana_services', () => ({ - getServices: () => ({ - history: () => ({ - location: { - search: '', - }, - }), - capabilities: { - visualize: { - show: true, - }, - discover: { - save: false, - }, - }, - uiSettings: { - get: (key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } - }, +const mockServices = ({ + history: () => ({ + location: { + search: '', }, }), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + }, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } + }, + }, +} as unknown) as DiscoverServices; + +jest.mock('../../../kibana_services', () => ({ + getServices: () => mockServices, })); jest.mock('./lib/get_index_pattern_field_list', () => ({ @@ -71,9 +77,9 @@ function getCompProps() { ); // @ts-expect-error _.each() is passing additional args to flattenHit - const hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit) as Array< + const hits = (each(cloneDeep(realHits), indexPattern.flattenHit) as Array< Record<string, unknown> - >; + >) as ElasticSearchHit[]; const indexPatternList = [ { id: '0', attributes: { title: 'b' } } as SavedObject<IndexPatternAttributes>, @@ -97,9 +103,12 @@ function getCompProps() { onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, + services: mockServices, setIndexPattern: jest.fn(), state: {}, trackUiMetric: jest.fn(), + fieldFilter: getDefaultFieldFilter(), + setFieldFilter: jest.fn(), }; } @@ -128,9 +137,4 @@ describe('discover sidebar', function () { findTestSubject(comp, 'fieldToggle-extension').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); - it('should allow adding filters', function () { - findTestSubject(comp, 'field-extension-showDetails').simulate('click'); - findTestSubject(comp, 'plus-extension-gif').simulate('click'); - expect(props.onAddFilter).toHaveBeenCalled(); - }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index b8e09ce4d17e8..3283551488d68 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -19,10 +19,19 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { sortBy } from 'lodash'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { UiStatsMetricType } from '@kbn/analytics'; +import { + EuiAccordion, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiTitle, + EuiSpacer, + EuiNotificationBadge, + EuiPageSideBar, +} from '@elastic/eui'; +import { isEqual, sortBy } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; @@ -32,11 +41,16 @@ import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; -import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; +import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; -import { getServices } from '../../../kibana_services'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; export interface DiscoverSidebarProps { + /** + * Determines whether add/remove buttons are displayed not only when focused + */ + alwaysShowActionButtons?: boolean; /** * the selected columns displayed in the doc table in discover */ @@ -45,10 +59,14 @@ export interface DiscoverSidebarProps { * a statistics of the distribution of fields in the given hits */ fieldCounts: Record<string, number>; + /** + * Current state of the field filter, filtering fields by name, type, ... + */ + fieldFilter: FieldFilterState; /** * hits fetched from ES, displayed in the doc table */ - hits: Array<Record<string, unknown>>; + hits: ElasticSearchHit[]; /** * List of available index patterns */ @@ -70,6 +88,14 @@ export interface DiscoverSidebarProps { * Currently selected index pattern */ selectedIndexPattern?: IndexPattern; + /** + * Discover plugin services; + */ + services: DiscoverServices; + /** + * Change current state of fieldFilter + */ + setFieldFilter: (next: FieldFilterState) => void; /** * Callback function to select another index pattern */ @@ -80,35 +106,41 @@ export interface DiscoverSidebarProps { * @param eventName */ trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; + /** + * Shows index pattern and a button that displays the sidebar in a flyout + */ + useFlyout?: boolean; } export function DiscoverSidebar({ + alwaysShowActionButtons = false, columns, fieldCounts, + fieldFilter, hits, indexPatternList, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, + services, + setFieldFilter, setIndexPattern, trackUiMetric, + useFlyout = false, }: DiscoverSidebarProps) { - const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState<IndexPatternField[] | null>(null); - const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); - const services = useMemo(() => getServices(), []); useEffect(() => { const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts); setFields(newFields); - }, [selectedIndexPattern, fieldCounts, hits, services]); + }, [selectedIndexPattern, fieldCounts, hits]); const onChangeFieldSearch = useCallback( (field: string, value: string | boolean | undefined) => { - const newState = setFieldFilterProp(fieldFilterState, field, value); - setFieldFilterState(newState); + const newState = setFieldFilterProp(fieldFilter, field, value); + setFieldFilter(newState); }, - [fieldFilterState] + [fieldFilter, setFieldFilter] ); const getDetailsByField = useCallback( @@ -122,12 +154,12 @@ export function DiscoverSidebar({ selected: selectedFields, popular: popularFields, unpopular: unpopularFields, - } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilterState), [ + } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter), [ fields, columns, popularLimit, fieldCounts, - fieldFilterState, + fieldFilter, ]); const fieldTypes = useMemo(() => { @@ -146,10 +178,11 @@ export function DiscoverSidebar({ return null; } - return ( - <I18nProvider> + const filterChanged = isEqual(fieldFilter, getDefaultFieldFilter()); + + if (useFlyout) { + return ( <section - className="sidebar-list" aria-label={i18n.translate('discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel', { defaultMessage: 'Index and fields', })} @@ -159,159 +192,191 @@ export function DiscoverSidebar({ setIndexPattern={setIndexPattern} indexPatternList={sortBy(indexPatternList, (o) => o.attributes.title)} /> - <div className="dscSidebar__item"> + </section> + ); + } + + return ( + <EuiPageSideBar + className="dscSidebar" + aria-label={i18n.translate('discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel', { + defaultMessage: 'Index and fields', + })} + id="discover-sidebar" + data-test-subj="discover-sidebar" + > + <EuiFlexGroup + className="dscSidebar__group" + direction="column" + alignItems="stretch" + gutterSize="s" + responsive={false} + > + <EuiFlexItem grow={false}> + <DiscoverIndexPattern + selectedIndexPattern={selectedIndexPattern} + setIndexPattern={setIndexPattern} + indexPatternList={sortBy(indexPatternList, (o) => o.attributes.title)} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> <form> <DiscoverFieldSearch onChange={onChangeFieldSearch} - value={fieldFilterState.name} + value={fieldFilter.name} types={fieldTypes} /> </form> - </div> - <div className="sidebar-list"> - {fields.length > 0 && ( - <> - <EuiTitle size="xxxs" id="selected_fields"> - <h3> - <FormattedMessage - id="discover.fieldChooser.filter.selectedFieldsTitle" - defaultMessage="Selected fields" - /> - </h3> - </EuiTitle> - <EuiSpacer size="xs" /> - <ul - className="dscSidebarList dscFieldList--selected" - aria-labelledby="selected_fields" - data-test-subj={`fieldList-selected`} - > - {selectedFields.map((field: IndexPatternField) => { - return ( - <li - key={`field${field.name}`} - data-attr-field={field.name} - className="dscSidebar__item" - > - <DiscoverField - field={field} - indexPattern={selectedIndexPattern} - onAddField={onAddField} - onRemoveField={onRemoveField} - onAddFilter={onAddFilter} - getDetails={getDetailsByField} - selected={true} - trackUiMetric={trackUiMetric} - /> - </li> - ); - })} - </ul> - <div className="euiFlexGroup euiFlexGroup--gutterMedium"> - <EuiTitle size="xxxs" id="available_fields" className="euiFlexItem"> - <h3> - <FormattedMessage - id="discover.fieldChooser.filter.availableFieldsTitle" - defaultMessage="Available fields" - /> - </h3> - </EuiTitle> - <div className="euiFlexItem euiFlexItem--flexGrowZero"> - <EuiButtonIcon - className={'visible-xs visible-sm dscFieldChooser__toggle'} - iconType={showFields ? 'arrowDown' : 'arrowRight'} - onClick={() => setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> - </div> - </div> - </> - )} - {popularFields.length > 0 && ( - <div> - <EuiTitle - size="xxxs" - className={`dscFieldListHeader ${!showFields ? 'hidden-sm hidden-xs' : ''}`} - > - <h4 style={{ fontWeight: 'normal' }} id="available_fields_popular"> - <FormattedMessage - id="discover.fieldChooser.filter.popularTitle" - defaultMessage="Popular" - /> - </h4> - </EuiTitle> - <ul - className={`dscFieldList dscFieldList--popular ${ - !showFields ? 'hidden-sm hidden-xs' : '' - }`} - aria-labelledby="available_fields available_fields_popular" - data-test-subj={`fieldList-popular`} - > - {popularFields.map((field: IndexPatternField) => { - return ( - <li - key={`field${field.name}`} - data-attr-field={field.name} - className="dscSidebar__item" + </EuiFlexItem> + <EuiFlexItem className="eui-yScroll"> + <div> + {fields.length > 0 && ( + <> + {selectedFields && + selectedFields.length > 0 && + selectedFields[0].displayName !== '_source' ? ( + <> + <EuiAccordion + id="dscSelectedFields" + initialIsOpen={true} + buttonContent={ + <EuiText size="xs" id="selected_fields"> + <strong> + <FormattedMessage + id="discover.fieldChooser.filter.selectedFieldsTitle" + defaultMessage="Selected fields" + /> + </strong> + </EuiText> + } + extraAction={ + <EuiNotificationBadge color={filterChanged ? 'subdued' : 'accent'} size="m"> + {selectedFields.length} + </EuiNotificationBadge> + } > - <DiscoverField - field={field} - indexPattern={selectedIndexPattern} - onAddField={onAddField} - onRemoveField={onRemoveField} - onAddFilter={onAddFilter} - getDetails={getDetailsByField} - trackUiMetric={trackUiMetric} - /> - </li> - ); - })} - </ul> - </div> - )} - - <ul - className={`dscFieldList dscFieldList--unpopular ${ - !showFields ? 'hidden-sm hidden-xs' : '' - }`} - aria-labelledby="available_fields" - data-test-subj={`fieldList-unpopular`} - > - {unpopularFields.map((field: IndexPatternField) => { - return ( - <li - key={`field${field.name}`} - data-attr-field={field.name} - className="dscSidebar__item" + <EuiSpacer size="m" /> + <ul + className="dscFieldList" + aria-labelledby="selected_fields" + data-test-subj={`fieldList-selected`} + > + {selectedFields.map((field: IndexPatternField) => { + return ( + <li + key={`field${field.name}`} + data-attr-field={field.name} + className="dscSidebar__item" + > + <DiscoverField + alwaysShowActionButton={alwaysShowActionButtons} + field={field} + indexPattern={selectedIndexPattern} + onAddField={onAddField} + onRemoveField={onRemoveField} + onAddFilter={onAddFilter} + getDetails={getDetailsByField} + selected={true} + trackUiMetric={trackUiMetric} + /> + </li> + ); + })} + </ul> + </EuiAccordion> + <EuiSpacer size="s" />{' '} + </> + ) : null} + <EuiAccordion + id="dscAvailableFields" + initialIsOpen={true} + buttonContent={ + <EuiText size="xs" id="available_fields"> + <strong> + <FormattedMessage + id="discover.fieldChooser.filter.availableFieldsTitle" + defaultMessage="Available fields" + /> + </strong> + </EuiText> + } + extraAction={ + <EuiNotificationBadge size="m" color={filterChanged ? 'subdued' : 'accent'}> + {popularFields.length + unpopularFields.length} + </EuiNotificationBadge> + } > - <DiscoverField - field={field} - indexPattern={selectedIndexPattern} - onAddField={onAddField} - onRemoveField={onRemoveField} - onAddFilter={onAddFilter} - getDetails={getDetailsByField} - trackUiMetric={trackUiMetric} - /> - </li> - ); - })} - </ul> - </div> - </section> - </I18nProvider> + <EuiSpacer size="s" /> + {popularFields.length > 0 && ( + <> + <EuiTitle size="xxxs" className="dscFieldListHeader"> + <h4 id="available_fields_popular"> + <FormattedMessage + id="discover.fieldChooser.filter.popularTitle" + defaultMessage="Popular" + /> + </h4> + </EuiTitle> + <ul + className="dscFieldList dscFieldList--popular" + aria-labelledby="available_fields available_fields_popular" + data-test-subj={`fieldList-popular`} + > + {popularFields.map((field: IndexPatternField) => { + return ( + <li + key={`field${field.name}`} + data-attr-field={field.name} + className="dscSidebar__item" + > + <DiscoverField + alwaysShowActionButton={alwaysShowActionButtons} + field={field} + indexPattern={selectedIndexPattern} + onAddField={onAddField} + onRemoveField={onRemoveField} + onAddFilter={onAddFilter} + getDetails={getDetailsByField} + trackUiMetric={trackUiMetric} + /> + </li> + ); + })} + </ul> + </> + )} + <ul + className="dscFieldList dscFieldList--unpopular" + aria-labelledby="available_fields" + data-test-subj={`fieldList-unpopular`} + > + {unpopularFields.map((field: IndexPatternField) => { + return ( + <li + key={`field${field.name}`} + data-attr-field={field.name} + className="dscSidebar__item" + > + <DiscoverField + alwaysShowActionButton={alwaysShowActionButtons} + field={field} + indexPattern={selectedIndexPattern} + onAddField={onAddField} + onRemoveField={onRemoveField} + onAddFilter={onAddFilter} + getDetails={getDetailsByField} + trackUiMetric={trackUiMetric} + /> + </li> + ); + })} + </ul> + </EuiAccordion> + </> + )} + </div> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPageSideBar> ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx new file mode 100644 index 0000000000000..906de04df3a1d --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { each, cloneDeep } from 'lodash'; +import { ReactWrapper } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import realHits from 'fixtures/real_hits.js'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from '@kbn/test/jest'; +import React from 'react'; +import { DiscoverSidebarProps } from './discover_sidebar'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +import { SavedObject } from '../../../../../../core/types'; +import { FieldFilterState } from './lib/field_filter'; +import { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; + +const mockServices = ({ + history: () => ({ + location: { + search: '', + }, + }), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + }, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } + }, + }, +} as unknown) as DiscoverServices; + +jest.mock('../../../kibana_services', () => ({ + getServices: () => mockServices, +})); + +jest.mock('./lib/get_index_pattern_field_list', () => ({ + getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), +})); + +function getCompProps() { + const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + + // @ts-expect-error _.each() is passing additional args to flattenHit + const hits = (each(cloneDeep(realHits), indexPattern.flattenHit) as Array< + Record<string, unknown> + >) as ElasticSearchHit[]; + + const indexPatternList = [ + { id: '0', attributes: { title: 'b' } } as SavedObject<IndexPatternAttributes>, + { id: '1', attributes: { title: 'a' } } as SavedObject<IndexPatternAttributes>, + { id: '2', attributes: { title: 'c' } } as SavedObject<IndexPatternAttributes>, + ]; + + const fieldCounts: Record<string, number> = {}; + + for (const hit of hits) { + for (const key of Object.keys(indexPattern.flattenHit(hit))) { + fieldCounts[key] = (fieldCounts[key] || 0) + 1; + } + } + return { + columns: ['extension'], + fieldCounts, + hits, + indexPatternList, + onAddFilter: jest.fn(), + onAddField: jest.fn(), + onRemoveField: jest.fn(), + selectedIndexPattern: indexPattern, + services: mockServices, + setIndexPattern: jest.fn(), + state: {}, + trackUiMetric: jest.fn(), + fieldFilter: {} as FieldFilterState, + setFieldFilter: jest.fn(), + }; +} + +describe('discover responsive sidebar', function () { + let props: DiscoverSidebarProps; + let comp: ReactWrapper<DiscoverSidebarProps>; + + beforeAll(() => { + props = getCompProps(); + comp = mountWithIntl(<DiscoverSidebarResponsive {...props} />); + }); + + it('should have Selected Fields and Available Fields with Popular Fields sections', function () { + const popular = findTestSubject(comp, 'fieldList-popular'); + const selected = findTestSubject(comp, 'fieldList-selected'); + const unpopular = findTestSubject(comp, 'fieldList-unpopular'); + expect(popular.children().length).toBe(1); + expect(unpopular.children().length).toBe(7); + expect(selected.children().length).toBe(1); + }); + it('should allow selecting fields', function () { + findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); + }); + it('should allow deselecting fields', function () { + findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + expect(props.onRemoveField).toHaveBeenCalledWith('extension'); + }); + it('should allow adding filters', function () { + findTestSubject(comp, 'field-extension-showDetails').simulate('click'); + findTestSubject(comp, 'plus-extension-gif').simulate('click'); + expect(props.onAddFilter).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx new file mode 100644 index 0000000000000..369ebbde5743b --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -0,0 +1,205 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState } from 'react'; +import { sortBy } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { UiStatsMetricType } from '@kbn/analytics'; +import { + EuiTitle, + EuiHideFor, + EuiShowFor, + EuiButton, + EuiBadge, + EuiFlyoutHeader, + EuiFlyout, + EuiSpacer, + EuiIcon, + EuiLink, + EuiPortal, +} from '@elastic/eui'; +import { DiscoverIndexPattern } from './discover_index_pattern'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { SavedObject } from '../../../../../../core/types'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { getDefaultFieldFilter } from './lib/field_filter'; +import { DiscoverSidebar } from './discover_sidebar'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; + +export interface DiscoverSidebarResponsiveProps { + /** + * Determines whether add/remove buttons are displayed non only when focused + */ + alwaysShowActionButtons?: boolean; + /** + * the selected columns displayed in the doc table in discover + */ + columns: string[]; + /** + * a statistics of the distribution of fields in the given hits + */ + fieldCounts: Record<string, number>; + /** + * hits fetched from ES, displayed in the doc table + */ + hits: ElasticSearchHit[]; + /** + * List of available index patterns + */ + indexPatternList: Array<SavedObject<IndexPatternAttributes>>; + /** + * Has been toggled closed + */ + isClosed?: boolean; + /** + * Callback function when selecting a field + */ + onAddField: (fieldName: string) => void; + /** + * Callback function when adding a filter from sidebar + */ + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Callback function when removing a field + * @param fieldName + */ + onRemoveField: (fieldName: string) => void; + /** + * Currently selected index pattern + */ + selectedIndexPattern?: IndexPattern; + /** + * Discover plugin services; + */ + services: DiscoverServices; + /** + * Callback function to select another index pattern + */ + setIndexPattern: (id: string) => void; + /** + * Metric tracking function + * @param metricType + * @param eventName + */ + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; + /** + * Shows index pattern and a button that displays the sidebar in a flyout + */ + useFlyout?: boolean; +} + +/** + * Component providing 2 different renderings for the sidebar depending on available screen space + * Desktop: Sidebar view, all elements are visible + * Mobile: Index pattern selector is visible and a button to trigger a flyout with all elements + */ +export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { + const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + if (!props.selectedIndexPattern) { + return null; + } + + return ( + <> + {props.isClosed ? null : ( + <EuiHideFor sizes={['xs', 's']}> + <DiscoverSidebar {...props} fieldFilter={fieldFilter} setFieldFilter={setFieldFilter} /> + </EuiHideFor> + )} + <EuiShowFor sizes={['xs', 's']}> + <div className="dscSidebar__mobile"> + <section + aria-label={i18n.translate( + 'discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel', + { + defaultMessage: 'Index and fields', + } + )} + > + <DiscoverIndexPattern + selectedIndexPattern={props.selectedIndexPattern} + setIndexPattern={props.setIndexPattern} + indexPatternList={sortBy(props.indexPatternList, (o) => o.attributes.title)} + /> + </section> + <EuiSpacer size="s" /> + <EuiButton + contentProps={{ className: 'dscSidebar__mobileButton' }} + fullWidth + onClick={() => setIsFlyoutVisible(true)} + > + <FormattedMessage + id="discover.fieldChooser.fieldsMobileButtonLabel" + defaultMessage="Fields" + /> + <EuiBadge + className="dscSidebar__mobileBadge" + color={props.columns[0] === '_source' ? 'default' : 'accent'} + > + {props.columns[0] === '_source' ? 0 : props.columns.length} + </EuiBadge> + </EuiButton> + </div> + {isFlyoutVisible && ( + <EuiPortal> + <EuiFlyout + size="s" + onClose={() => setIsFlyoutVisible(false)} + aria-labelledby="flyoutTitle" + ownFocus + > + <EuiFlyoutHeader hasBorder> + <EuiTitle size="s"> + <h2 id="flyoutTitle"> + <EuiLink color="text" onClick={() => setIsFlyoutVisible(false)}> + <EuiIcon + className="eui-alignBaseline" + aria-label={i18n.translate('discover.fieldList.flyoutBackIcon', { + defaultMessage: 'Back', + })} + type="arrowLeft" + />{' '} + <strong> + {i18n.translate('discover.fieldList.flyoutHeading', { + defaultMessage: 'Field list', + })} + </strong> + </EuiLink> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + {/* Using only the direct flyout body class because we maintain scroll in a lower sidebar component. Needs a fix on the EUI side */} + <div className="euiFlyoutBody"> + <DiscoverSidebar + {...props} + fieldFilter={fieldFilter} + setFieldFilter={setFieldFilter} + alwaysShowActionButtons={true} + /> + </div> + </EuiFlyout> + </EuiPortal> + )} + </EuiShowFor> + </> + ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/index.ts b/src/plugins/discover/public/application/components/sidebar/index.ts index aec8dfc86e817..7575b5691a95a 100644 --- a/src/plugins/discover/public/application/components/sidebar/index.ts +++ b/src/plugins/discover/public/application/components/sidebar/index.ts @@ -18,3 +18,4 @@ */ export { DiscoverSidebar } from './discover_sidebar'; +export { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index 22a6e7a628555..e979131a7a85f 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -20,10 +20,11 @@ // @ts-ignore import { fieldCalculator } from './field_calculator'; import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; export function getDetails( field: IndexPatternField, - hits: Array<Record<string, unknown>>, + hits: ElasticSearchHit[], columns: string[], indexPattern?: IndexPattern ) { diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx index d5bc5bb64f59b..e2b8e0ffcf518 100644 --- a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { EuiSkipLink } from '@elastic/eui'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; export interface SkipBottomButtonProps { /** @@ -29,26 +29,22 @@ export interface SkipBottomButtonProps { export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { return ( - <I18nProvider> - <EuiSkipLink - size="s" - // @ts-ignore - onClick={(event) => { - // prevent the anchor to reload the page on click - event.preventDefault(); - // The destinationId prop cannot be leveraged here as the table needs - // to be updated first (angular logic) - onClick(); - }} - className="dscSkipButton" - destinationId="" - data-test-subj="discoverSkipTableButton" - > - <FormattedMessage - id="discover.skipToBottomButtonLabel" - defaultMessage="Skip to end of table" - /> - </EuiSkipLink> - </I18nProvider> + <EuiSkipLink + size="s" + onClick={(event: React.MouseEvent<HTMLButtonElement>) => { + // prevent the anchor to reload the page on click + event.preventDefault(); + // The destinationId prop cannot be leveraged here as the table needs + // to be updated first (angular logic) + onClick(); + }} + className="dscSkipButton" + id="dscSkipButton" + destinationId="" + data-test-subj="discoverSkipTableButton" + position="absolute" + > + <FormattedMessage id="discover.skipToBottomButtonLabel" defaultMessage="Go to end of table" /> + </EuiSkipLink> ); } diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 5d37f598b38f6..d57447eab9e26 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -32,13 +32,16 @@ export function DocViewTable({ onAddColumn, onRemoveColumn, }: DocViewRenderProps) { + const [fieldRowOpen, setFieldRowOpen] = useState({} as Record<string, boolean>); + if (!indexPattern) { + return null; + } const mapping = indexPattern.fields.getByName; const flattened = indexPattern.flattenHit(hit); const formatted = indexPattern.formatHit(hit, 'html'); - const [fieldRowOpen, setFieldRowOpen] = useState({} as Record<string, boolean>); function toggleValueCollapse(field: string) { - fieldRowOpen[field] = fieldRowOpen[field] !== true; + fieldRowOpen[field] = !fieldRowOpen[field]; setFieldRowOpen({ ...fieldRowOpen }); } diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 3d75e175951d5..3ebf3c435916b 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -67,32 +67,11 @@ export function DocViewTableRow({ return ( <tr key={field} data-test-subj={`tableDocViewRow-${field}`}> - {typeof onFilter === 'function' && ( - <td className="kbnDocViewer__buttons"> - <DocViewTableRowBtnFilterAdd - disabled={!fieldMapping || !fieldMapping.filterable} - onClick={() => onFilter(fieldMapping, valueRaw, '+')} - /> - <DocViewTableRowBtnFilterRemove - disabled={!fieldMapping || !fieldMapping.filterable} - onClick={() => onFilter(fieldMapping, valueRaw, '-')} - /> - {typeof onToggleColumn === 'function' && ( - <DocViewTableRowBtnToggleColumn active={isColumnActive} onClick={onToggleColumn} /> - )} - <DocViewTableRowBtnFilterExists - disabled={!fieldMapping || !fieldMapping.filterable} - onClick={() => onFilter('_exists_', field, '+')} - scripted={fieldMapping && fieldMapping.scripted} - /> - </td> - )} <td className="kbnDocViewer__field"> <FieldName fieldName={field} fieldType={fieldType} fieldMapping={fieldMapping} - fieldIconProps={{ fill: 'none', color: 'gray' }} scripted={Boolean(fieldMapping?.scripted)} /> </td> @@ -113,6 +92,26 @@ export function DocViewTableRow({ dangerouslySetInnerHTML={{ __html: value as string }} /> </td> + {typeof onFilter === 'function' && ( + <td className="kbnDocViewer__buttons"> + <DocViewTableRowBtnFilterAdd + disabled={!fieldMapping || !fieldMapping.filterable} + onClick={() => onFilter(fieldMapping, valueRaw, '+')} + /> + <DocViewTableRowBtnFilterRemove + disabled={!fieldMapping || !fieldMapping.filterable} + onClick={() => onFilter(fieldMapping, valueRaw, '-')} + /> + {typeof onToggleColumn === 'function' && ( + <DocViewTableRowBtnToggleColumn active={isColumnActive} onClick={onToggleColumn} /> + )} + <DocViewTableRowBtnFilterExists + disabled={!fieldMapping || !fieldMapping.filterable} + onClick={() => onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + </td> + )} </tr> ); } diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx index bd842eb5c6f72..142761768b472 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx @@ -49,7 +49,7 @@ export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props data-test-subj="addInclusiveFilterButton" disabled={disabled} onClick={onClick} - iconType={'magnifyWithPlus'} + iconType={'plusInCircle'} iconSize={'s'} /> </EuiToolTip> diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx index dab22c103bc48..43a711fc72da5 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx @@ -61,7 +61,7 @@ export function DocViewTableRowBtnFilterExists({ className="kbnDocViewer__actionButton" data-test-subj="addExistsFilterButton" disabled={disabled} - iconType={'indexOpen'} + iconType={'filter'} iconSize={'s'} /> </EuiToolTip> diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx index bbef54cb4ecc7..878088ae0a6d8 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx @@ -49,7 +49,7 @@ export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Pr data-test-subj="removeInclusiveFilterButton" disabled={disabled} onClick={onClick} - iconType={'magnifyWithMinus'} + iconType={'minusInCircle'} iconSize={'s'} /> </EuiToolTip> diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx index 3e5a057929701..1a32ba3be1712 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx @@ -37,7 +37,7 @@ export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = fal className="kbnDocViewer__actionButton" data-test-subj="toggleColumnButton" disabled - iconType={'tableOfContents'} + iconType={'listAdd'} iconSize={'s'} /> ); @@ -59,7 +59,7 @@ export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = fal onClick={onClick} className="kbnDocViewer__actionButton" data-test-subj="toggleColumnButton" - iconType={'tableOfContents'} + iconType={'listAdd'} iconSize={'s'} /> </EuiToolTip> diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss b/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss new file mode 100644 index 0000000000000..506dc26d9bee3 --- /dev/null +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss @@ -0,0 +1,7 @@ +.dscTimeIntervalSelect { + align-items: center; +} + +.dscTimeChartHeader { + flex-grow: 0; +} diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx index 1451106827ee0..544de61b5825b 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx @@ -25,8 +25,8 @@ import { EuiSelect, EuiIconTip, } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import './timechart_header.scss'; import moment from 'moment'; export interface TimechartHeaderProps { @@ -99,73 +99,78 @@ export function TimechartHeader({ } return ( - <I18nProvider> - <EuiFlexGroup gutterSize="s" responsive justifyContent="center" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiToolTip - content={i18n.translate('discover.howToChangeTheTimeTooltip', { - defaultMessage: 'To change the time, use the global time filter above', + <EuiFlexGroup + className="dscTimeChartHeader" + gutterSize="s" + responsive={false} + wrap + justifyContent="center" + alignItems="center" + > + <EuiFlexItem grow={false} className="eui-hideFor--m"> + <EuiToolTip + content={i18n.translate('discover.howToChangeTheTimeTooltip', { + defaultMessage: 'To change the time, use the global time filter.', + })} + delay="long" + > + <EuiText data-test-subj="discoverIntervalDateRange" textAlign="center" size="s"> + {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ + interval !== 'auto' + ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { + defaultMessage: 'per', + }) + : '' + }`} + </EuiText> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem className="dscTimeIntervalSelect" grow={false}> + <EuiSelect + aria-label={i18n.translate('discover.timechartHeader.timeIntervalSelect.ariaLabel', { + defaultMessage: 'Time interval', + })} + compressed + id="dscResultsIntervalSelector" + data-test-subj="discoverIntervalSelect" + options={options + .filter(({ val }) => val !== 'custom') + .map(({ display, val }) => { + return { + text: display, + value: val, + label: display, + }; })} - delay="long" - > - <EuiText data-test-subj="discoverIntervalDateRange" size="s"> - {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ - interval !== 'auto' - ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { - defaultMessage: 'per', - }) - : '' - }`} - </EuiText> - </EuiToolTip> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSelect - aria-label={i18n.translate('discover.timechartHeader.timeIntervalSelect.ariaLabel', { - defaultMessage: 'Time interval', - })} - compressed - id="dscResultsIntervalSelector" - data-test-subj="discoverIntervalSelect" - options={options - .filter(({ val }) => val !== 'custom') - .map(({ display, val }) => { - return { - text: display, - value: val, - label: display, - }; - })} - value={interval} - onChange={handleIntervalChange} - append={ - bucketInterval.scaled ? ( - <EuiIconTip - id="discoverIntervalIconTip" - content={i18n.translate('discover.bucketIntervalTooltip', { - defaultMessage: - 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', - values: { - bucketsDescription: - bucketInterval!.scale && bucketInterval!.scale > 1 - ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { - defaultMessage: 'buckets that are too large', - }) - : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { - defaultMessage: 'too many buckets', - }), - bucketIntervalDescription: bucketInterval.description, - }, - })} - color="warning" - size="s" - type="alert" - /> - ) : undefined - } - /> - </EuiFlexItem> - </EuiFlexGroup> - </I18nProvider> + value={interval} + onChange={handleIntervalChange} + append={ + bucketInterval.scaled ? ( + <EuiIconTip + id="discoverIntervalIconTip" + content={i18n.translate('discover.bucketIntervalTooltip', { + defaultMessage: + 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', + values: { + bucketsDescription: + bucketInterval!.scale && bucketInterval!.scale > 1 + ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { + defaultMessage: 'buckets that are too large', + }) + : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { + defaultMessage: 'too many buckets', + }), + bucketIntervalDescription: bucketInterval.description, + }, + })} + color="warning" + size="s" + type="alert" + /> + ) : undefined + } + /> + </EuiFlexItem> + </EuiFlexGroup> ); } diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 01145402e0f29..dcfc25fd4099d 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -49,7 +49,7 @@ export interface DocViewRenderProps { columns?: string[]; filter?: DocViewFilterFn; hit: ElasticSearchHit; - indexPattern: IndexPattern; + indexPattern?: IndexPattern; onAddColumn?: (columnName: string) => void; onRemoveColumn?: (columnName: string) => void; } diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover/public/application/index.scss index 5aa353828274c..3c24d4f51de2e 100644 --- a/src/plugins/discover/public/application/index.scss +++ b/src/plugins/discover/public/application/index.scss @@ -1,2 +1 @@ @import 'angular/index'; -@import 'discover'; diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 2270f3c815aaa..78197cd8d66ff 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -114,7 +114,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.waitUntilSearchingHasFinished(); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(Math.round(newDurationHours)).to.be(24); + expect(Math.round(newDurationHours)).to.be(26); await retry.waitFor('doc table to contain the right search result', async () => { const rowData = await PageObjects.discover.getDocTableField(1); diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.ts similarity index 65% rename from test/functional/apps/discover/_sidebar.js rename to test/functional/apps/discover/_sidebar.ts index ce7ebff9cce74..c91c9020b373b 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.ts @@ -17,31 +17,23 @@ * under the License. */ -import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { - const log = getService('log'); +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const testSubjects = getService('testSubjects'); describe('discover sidebar', function describeIndexTests() { before(async function () { - // delete .kibana index and update configDoc + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('discover'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', }); - - log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); - - // and load a set of makelogs data - await esArchiver.loadIfNeeded('logstash_functional'); - - log.debug('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - - await PageObjects.timePicker.setDefaultAbsoluteRange(); }); describe('field filtering', function () { @@ -53,26 +45,17 @@ export default function ({ getService, getPageObjects }) { describe('collapse expand', function () { it('should initially be expanded', async function () { - const width = await PageObjects.discover.getSidebarWidth(); - log.debug('expanded sidebar width = ' + width); - expect(width > 20).to.be(true); + await testSubjects.existOrFail('discover-sidebar'); }); it('should collapse when clicked', async function () { await PageObjects.discover.toggleSidebarCollapse(); - log.debug('PageObjects.discover.getSidebarWidth()'); - const width = await PageObjects.discover.getSidebarWidth(); - log.debug('collapsed sidebar width = ' + width); - expect(width < 20).to.be(true); + await testSubjects.missingOrFail('discover-sidebar'); }); it('should expand when clicked', async function () { await PageObjects.discover.toggleSidebarCollapse(); - - log.debug('PageObjects.discover.getSidebarWidth()'); - const width = await PageObjects.discover.getSidebarWidth(); - log.debug('expanded sidebar width = ' + width); - expect(width > 20).to.be(true); + await testSubjects.existOrFail('discover-sidebar'); }); }); }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 9c5bedf7c242d..494141355806f 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -251,11 +251,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider .map((field) => $(field).text()); } - public async getSidebarWidth() { - const sidebar = await testSubjects.find('discover-sidebar'); - return await sidebar.getAttribute('clientWidth'); - } - public async hasNoResults() { return await testSubjects.exists('discoverNoResults'); } @@ -284,6 +279,9 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async clickFieldListItemRemove(field: string) { + if (!(await testSubjects.exists('fieldList-selected'))) { + return; + } const selectedList = await testSubjects.find('fieldList-selected'); if (await testSubjects.descendantExists(`field-${field}`, selectedList)) { await this.clickFieldListItemToggle(field); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 43f18a2040dab..ce193db011544 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1486,15 +1486,12 @@ "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドはElasticsearchマッピングに表示されますが、ドキュメントテーブルの{hitsLength}件のドキュメントには含まれません。可視化や検索は可能な場合があります。", - "discover.fieldChooser.fieldFilterFacetButtonLabel": "タイプでフィルタリング", "discover.fieldChooser.filter.aggregatableLabel": "集約可能", "discover.fieldChooser.filter.availableFieldsTitle": "利用可能なフィールド", "discover.fieldChooser.filter.fieldSelectorLabel": "{id}フィルターオプションの選択", "discover.fieldChooser.filter.filterByTypeLabel": "タイプでフィルタリング", "discover.fieldChooser.filter.hideMissingFieldsLabel": "未入力のフィールドを非表示", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "インデックスとフィールド", - "discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel": "フィールドを非表示", - "discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel": "フィールドを表示", "discover.fieldChooser.filter.popularTitle": "人気", "discover.fieldChooser.filter.searchableLabel": "検索可能", "discover.fieldChooser.filter.selectedFieldsTitle": "スクリプトフィールド", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f11c9c8b39db7..3414350044f17 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1487,15 +1487,12 @@ "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。", "discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。", "discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于它可视化或搜索。", - "discover.fieldChooser.fieldFilterFacetButtonLabel": "按类型筛选", "discover.fieldChooser.filter.aggregatableLabel": "可聚合", "discover.fieldChooser.filter.availableFieldsTitle": "可用字段", "discover.fieldChooser.filter.fieldSelectorLabel": "{id} 筛选选项的选择", "discover.fieldChooser.filter.filterByTypeLabel": "按类型筛选", "discover.fieldChooser.filter.hideMissingFieldsLabel": "隐藏缺失字段", "discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "索引和字段", - "discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel": "隐藏字段", - "discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel": "显示字段", "discover.fieldChooser.filter.popularTitle": "常见", "discover.fieldChooser.filter.searchableLabel": "可搜索", "discover.fieldChooser.filter.selectedFieldsTitle": "选定字段", From 43dd4876f2142d61dcc99325ad1508ac4d7fd8aa Mon Sep 17 00:00:00 2001 From: Tiago Costa <tiagoffcc@hotmail.com> Date: Thu, 3 Dec 2020 14:50:00 +0000 Subject: [PATCH 099/107] skip flaky suite (#84906) --- .../client_integration/component_template_edit.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index 114cafe9defde..5f1f5230a3ef7 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -81,7 +81,8 @@ describe('<ComponentTemplateEdit />', () => { expect(nameInput.props().disabled).toEqual(true); }); - describe('form payload', () => { + // FLAKY: https://github.com/elastic/kibana/issues/84906 + describe.skip('form payload', () => { it('should send the correct payload with changed values', async () => { const { actions, component, form } = testBed; From e83bbfd28962ba30316c666cf0164aaeb7c448c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= <sabee77@gmail.com> Date: Thu, 3 Dec 2020 15:59:20 +0100 Subject: [PATCH 100/107] [Runtime fields] Add support in index template (#84184) Co-authored-by: Adam Locke <adam.locke@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../errors/es_error_parser.test.ts | 42 +++ .../errors/es_error_parser.ts | 52 ++++ .../errors/index.ts | 1 + .../global_flyout/global_flyout.tsx | 28 +- .../es_ui_shared/server/errors/index.ts | 2 +- src/plugins/es_ui_shared/server/index.ts | 2 +- .../public/application/app_context.tsx | 1 + .../client_integration/helpers/index.ts | 2 +- .../helpers/setup_environment.tsx | 25 +- .../component_templates/__jest__/index.ts | 2 +- .../helpers/mappings_editor.helpers.tsx | 174 ++++++++----- .../helpers/setup_environment.tsx | 95 +++++++ .../client_integration/mapped_fields.test.tsx | 46 +++- .../mappings_editor.test.tsx | 13 +- .../runtime_fields.test.tsx | 246 ++++++++++++++++++ .../document_fields_header.tsx | 2 +- .../field_parameters/analyzer_parameter.tsx | 6 +- .../document_fields/field_parameters/index.ts | 4 - .../painless_script_parameter.tsx | 80 ------ .../runtime_type_parameter.tsx | 105 -------- .../required_parameters_forms/index.ts | 2 - .../runtime_type.tsx | 18 -- .../fields/field_types/index.ts | 2 - .../fields/field_types/runtime_type.tsx | 19 -- .../fields/fields_list_item.tsx | 65 +++-- .../fields/fields_list_item_container.tsx | 9 +- .../mappings_editor/components/index.ts | 2 + .../runtime_fields/delete_field_provider.tsx | 89 +++++++ .../runtime_fields/empty_prompt.tsx | 64 +++++ .../components/runtime_fields/index.ts | 7 + .../runtime_fields/runtime_fields_list.tsx | 151 +++++++++++ .../runtimefields_list_item.tsx | 123 +++++++++ .../runtimefields_list_item_container.tsx | 46 ++++ .../mappings_editor/config_context.tsx | 45 ++++ .../constants/data_types_definition.tsx | 20 -- .../constants/field_options.tsx | 1 - .../constants/parameters_definition.tsx | 48 ---- .../index_settings_context.tsx | 34 --- .../lib/extract_mappings_definition.ts | 12 +- .../components/mappings_editor/lib/index.ts | 2 + .../mappings_editor/lib/mappings_validator.ts | 1 + .../mappings_editor/lib/utils.test.ts | 24 +- .../components/mappings_editor/lib/utils.ts | 37 ++- .../mappings_editor/mappings_editor.tsx | 42 ++- .../mappings_editor_context.tsx | 4 +- .../mappings_state_context.tsx | 4 + .../components/mappings_editor/reducer.ts | 78 ++++++ .../mappings_editor/shared_imports.ts | 12 +- .../mappings_editor/types/document_fields.ts | 19 +- .../components/mappings_editor/types/state.ts | 22 +- .../mappings_editor/use_state_listener.tsx | 30 ++- .../application/components/section_error.tsx | 8 +- .../components/wizard_steps/step_mappings.tsx | 3 + .../application/mount_management_section.ts | 1 + .../application/services/documentation.ts | 4 + .../plugins/index_management/server/plugin.ts | 3 +- .../component_templates/privileges.test.ts | 2 + .../api/templates/register_create_route.ts | 6 +- .../api/templates/register_simulate_route.ts | 6 +- .../api/templates/register_update_route.ts | 6 +- .../index_management/server/shared_imports.ts | 6 +- .../plugins/index_management/server/types.ts | 3 +- x-pack/plugins/runtime_fields/README.md | 41 ++- .../runtime_fields/public/components/index.ts | 5 +- .../runtime_field_editor.test.tsx | 81 +++++- .../runtime_field_editor.tsx | 7 +- .../index.ts | 5 +- ...ntime_field_editor_flyout_content.test.tsx | 15 +- .../runtime_field_editor_flyout_content.tsx | 19 +- .../runtime_field_form.test.tsx | 10 +- .../runtime_field_form/runtime_field_form.tsx | 130 ++++++++- .../components/runtime_field_form/schema.ts | 17 +- x-pack/plugins/runtime_fields/public/index.ts | 1 + .../public/lib/documentation.ts | 2 + .../runtime_fields/public/load_editor.tsx | 13 +- .../runtime_fields/public/shared_imports.ts | 3 + x-pack/plugins/runtime_fields/public/types.ts | 4 +- .../management/index_management/templates.js | 46 ++++ 78 files changed, 1850 insertions(+), 557 deletions(-) create mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.test.ts create mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/runtime_fields.test.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/empty_prompt.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/config_context.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.test.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.test.ts new file mode 100644 index 0000000000000..29369f74a459d --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.test.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { parseEsError } from './es_error_parser'; + +describe('ES error parser', () => { + test('should return all the cause of the error', () => { + const esError = `{ + "error": { + "reason": "Houston we got a problem", + "caused_by": { + "reason": "First reason", + "caused_by": { + "reason": "Second reason", + "caused_by": { + "reason": "Third reason" + } + } + } + } + }`; + + const parsedError = parseEsError(esError); + expect(parsedError.message).toEqual('Houston we got a problem'); + expect(parsedError.cause).toEqual(['First reason', 'Second reason', 'Third reason']); + }); +}); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts new file mode 100644 index 0000000000000..800a56bc007eb --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +interface ParsedError { + message: string; + cause: string[]; +} + +const getCause = (obj: any = {}, causes: string[] = []): string[] => { + const updated = [...causes]; + + if (obj.caused_by) { + updated.push(obj.caused_by.reason); + + // Recursively find all the "caused by" reasons + return getCause(obj.caused_by, updated); + } + + return updated.filter(Boolean); +}; + +export const parseEsError = (err: string): ParsedError => { + try { + const { error } = JSON.parse(err); + const cause = getCause(error); + return { + message: error.reason, + cause, + }; + } catch (e) { + return { + message: err, + cause: [], + }; + } +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts index 484dc17868ab0..e467930d3ad0b 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts @@ -19,3 +19,4 @@ export { isEsError } from './is_es_error'; export { handleEsError } from './handle_es_error'; +export { parseEsError } from './es_error_parser'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx index 4dd9cfcaff16b..2b3e6ab48992d 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx @@ -54,7 +54,7 @@ export const GlobalFlyoutProvider: React.FC = ({ children }) => { const [showFlyout, setShowFlyout] = useState(false); const [activeContent, setActiveContent] = useState<Content<any> | undefined>(undefined); - const { id, Component, props, flyoutProps } = activeContent ?? {}; + const { id, Component, props, flyoutProps, cleanUpFunc } = activeContent ?? {}; const addContent: Context['addContent'] = useCallback((content) => { setActiveContent((prev) => { @@ -77,11 +77,19 @@ export const GlobalFlyoutProvider: React.FC = ({ children }) => { const removeContent: Context['removeContent'] = useCallback( (contentId: string) => { + // Note: when we will actually deal with multi content then + // there will be more logic here! :) if (contentId === id) { + setActiveContent(undefined); + + if (cleanUpFunc) { + cleanUpFunc(); + } + closeFlyout(); } }, - [id, closeFlyout] + [id, closeFlyout, cleanUpFunc] ); const mergedFlyoutProps = useMemo(() => { @@ -130,14 +138,6 @@ export const useGlobalFlyout = () => { const contents = useRef<Set<string> | undefined>(undefined); const { removeContent, addContent: addContentToContext } = ctx; - useEffect(() => { - isMounted.current = true; - - return () => { - isMounted.current = false; - }; - }, []); - const getContents = useCallback(() => { if (contents.current === undefined) { contents.current = new Set(); @@ -153,6 +153,14 @@ export const useGlobalFlyout = () => { [getContents, addContentToContext] ); + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + useEffect(() => { return () => { if (!isMounted.current) { diff --git a/src/plugins/es_ui_shared/server/errors/index.ts b/src/plugins/es_ui_shared/server/errors/index.ts index 532e02774ff50..3533e96aaea3a 100644 --- a/src/plugins/es_ui_shared/server/errors/index.ts +++ b/src/plugins/es_ui_shared/server/errors/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { isEsError, handleEsError } from '../../__packages_do_not_import__/errors'; +export { isEsError, handleEsError, parseEsError } from '../../__packages_do_not_import__/errors'; diff --git a/src/plugins/es_ui_shared/server/index.ts b/src/plugins/es_ui_shared/server/index.ts index b2c9c85d956ba..2801d0569aa3f 100644 --- a/src/plugins/es_ui_shared/server/index.ts +++ b/src/plugins/es_ui_shared/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { isEsError, handleEsError } from './errors'; +export { isEsError, handleEsError, parseEsError } from './errors'; /** dummy plugin*/ export function plugin() { diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index c9337767365fa..a9cdb668ca35e 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -37,6 +37,7 @@ export interface AppDependencies { setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; uiSettings: CoreSetup['uiSettings']; urlGenerators: SharePluginStart['urlGenerators']; + docLinks: CoreStart['docLinks']; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts index 4e03adcbcbb44..10b5805a7ad2f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts @@ -9,7 +9,7 @@ import { setup as componentTemplateDetailsSetup } from './component_template_det export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest'; -export { setupEnvironment, appDependencies } from './setup_environment'; +export { setupEnvironment, componentTemplatesDependencies } from './setup_environment'; export const pageHelpers = { componentTemplateList: { setup: componentTemplatesListSetup }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index ac748e1b7dc2c..38832e6beb5f5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -15,6 +15,7 @@ import { } from '../../../../../../../../../../src/core/public/mocks'; import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; +import { AppContextProvider } from '../../../../../app_context'; import { MappingsEditorProvider } from '../../../../mappings_editor'; import { ComponentTemplatesProvider } from '../../../component_templates_context'; @@ -24,7 +25,12 @@ import { API_BASE_PATH } from './constants'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); const { GlobalFlyoutProvider } = GlobalFlyout; -export const appDependencies = { +// We provide the minimum deps required to make the tests pass +const appDependencies = { + docLinks: {} as any, +} as any; + +export const componentTemplatesDependencies = { httpClient: (mockHttpClient as unknown) as HttpSetup, apiBasePath: API_BASE_PATH, trackMetric: () => {}, @@ -44,11 +50,14 @@ export const setupEnvironment = () => { }; export const WithAppDependencies = (Comp: any) => (props: any) => ( - <MappingsEditorProvider> - <ComponentTemplatesProvider value={appDependencies}> - <GlobalFlyoutProvider> - <Comp {...props} /> - </GlobalFlyoutProvider> - </ComponentTemplatesProvider> - </MappingsEditorProvider> + <AppContextProvider value={appDependencies}> + <MappingsEditorProvider> + <ComponentTemplatesProvider value={componentTemplatesDependencies}> + <GlobalFlyoutProvider> + <Comp {...props} /> + </GlobalFlyoutProvider> + </ComponentTemplatesProvider> + </MappingsEditorProvider> + / + </AppContextProvider> ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts index ebd2cd9392568..a0cafbb6d4217 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { appDependencies as componentTemplatesMockDependencies } from './client_integration/helpers'; +export { componentTemplatesDependencies as componentTemplatesMockDependencies } from './client_integration/helpers'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index 9302e080028cc..14252fc34c4e5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -3,57 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; + import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; +import { registerTestBed, TestBed, findTestSubject } from '@kbn/test/jest'; -import { registerTestBed, TestBed } from '@kbn/test/jest'; -import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; +// This import needs to come first as it sets the jest.mock calls +import { WithAppDependencies } from './setup_environment'; import { getChildFieldsName } from '../../../lib'; +import { RuntimeField } from '../../../shared_imports'; import { MappingsEditor } from '../../../mappings_editor'; -import { MappingsEditorProvider } from '../../../mappings_editor_context'; - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - - return { - ...original, - // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, - // which does not produce a valid component wrapper - EuiComboBox: (props: any) => ( - <input - data-test-subj={props['data-test-subj'] || 'mockComboBox'} - data-currentvalue={props.selectedOptions} - onChange={async (syntheticEvent: any) => { - props.onChange([syntheticEvent['0']]); - }} - /> - ), - // Mocking EuiCodeEditor, which uses React Ace under the hood - EuiCodeEditor: (props: any) => ( - <input - data-test-subj={props['data-test-subj'] || 'mockCodeEditor'} - data-currentvalue={props.value} - onChange={(e: any) => { - props.onChange(e.jsonContent); - }} - /> - ), - // Mocking EuiSuperSelect to be able to easily change its value - // with a `myWrapper.simulate('change', { target: { value: 'someValue' } })` - EuiSuperSelect: (props: any) => ( - <input - data-test-subj={props['data-test-subj'] || 'mockSuperSelect'} - value={props.valueOfSelected} - onChange={(e) => { - props.onChange(e.target.value); - }} - /> - ), - }; -}); - -const { GlobalFlyoutProvider } = GlobalFlyout; export interface DomFields { [key: string]: { @@ -64,8 +23,9 @@ export interface DomFields { } const createActions = (testBed: TestBed<TestSubjects>) => { - const { find, form, component } = testBed; + const { find, exists, form, component } = testBed; + // --- Mapped fields --- const getFieldInfo = (testSubjectField: string): { name: string; type: string } => { const name = find(`${testSubjectField}-fieldName` as TestSubjects).text(); const type = find(`${testSubjectField}-datatype` as TestSubjects).props()['data-type-value']; @@ -206,8 +166,102 @@ const createActions = (testBed: TestBed<TestSubjects>) => { component.update(); }; - const selectTab = async (tab: 'fields' | 'templates' | 'advanced') => { - const index = ['fields', 'templates', 'advanced'].indexOf(tab); + // --- Runtime fields --- + const openRuntimeFieldEditor = () => { + find('createRuntimeFieldButton').simulate('click'); + component.update(); + }; + + const updateRuntimeFieldForm = async (field: RuntimeField) => { + const valueToLabelMap = { + keyword: 'Keyword', + date: 'Date', + ip: 'IP', + long: 'Long', + double: 'Double', + boolean: 'Boolean', + }; + + if (!exists('runtimeFieldEditor')) { + throw new Error(`Can't update runtime field form as the editor is not opened.`); + } + + await act(async () => { + form.setInputValue('runtimeFieldEditor.nameField.input', field.name); + form.setInputValue('runtimeFieldEditor.scriptField', field.script.source); + find('typeField').simulate('change', [ + { + label: valueToLabelMap[field.type], + value: field.type, + }, + ]); + }); + }; + + const getRuntimeFieldsList = () => { + const fields = find('runtimeFieldsListItem').map((wrapper) => wrapper); + return fields.map((field) => { + return { + reactWrapper: field, + name: findTestSubject(field, 'fieldName').text(), + type: findTestSubject(field, 'fieldType').text(), + }; + }); + }; + + /** + * Open the editor, fill the form and close the editor + * @param field the field to add + */ + const addRuntimeField = async (field: RuntimeField) => { + openRuntimeFieldEditor(); + + await updateRuntimeFieldForm(field); + + await act(async () => { + find('runtimeFieldEditor.saveFieldButton').simulate('click'); + }); + component.update(); + }; + + const deleteRuntimeField = async (name: string) => { + const runtimeField = getRuntimeFieldsList().find((field) => field.name === name); + + if (!runtimeField) { + throw new Error(`Runtime field "${name}" to delete not found.`); + } + + await act(async () => { + findTestSubject(runtimeField.reactWrapper, 'removeFieldButton').simulate('click'); + }); + component.update(); + + // Modal is opened, confirm deletion + const modal = find('runtimeFieldDeleteConfirmModal'); + + act(() => { + findTestSubject(modal, 'confirmModalConfirmButton').simulate('click'); + }); + + component.update(); + }; + + const startEditRuntimeField = async (name: string) => { + const runtimeField = getRuntimeFieldsList().find((field) => field.name === name); + + if (!runtimeField) { + throw new Error(`Runtime field "${name}" to edit not found.`); + } + + await act(async () => { + findTestSubject(runtimeField.reactWrapper, 'editFieldButton').simulate('click'); + }); + component.update(); + }; + + // --- Other --- + const selectTab = async (tab: 'fields' | 'runtimeFields' | 'templates' | 'advanced') => { + const index = ['fields', 'runtimeFields', 'templates', 'advanced'].indexOf(tab); const tabElement = find('formTab').at(index); if (tabElement.length === 0) { @@ -268,19 +322,17 @@ const createActions = (testBed: TestBed<TestSubjects>) => { getToggleValue, getCheckboxValue, toggleFormRow, + openRuntimeFieldEditor, + getRuntimeFieldsList, + updateRuntimeFieldForm, + addRuntimeField, + deleteRuntimeField, + startEditRuntimeField, }; }; export const setup = (props: any = { onUpdate() {} }): MappingsEditorTestBed => { - const ComponentToTest = (propsOverride: { [key: string]: any }) => ( - <MappingsEditorProvider> - <GlobalFlyoutProvider> - <MappingsEditor {...props} {...propsOverride} /> - </GlobalFlyoutProvider> - </MappingsEditorProvider> - ); - - const setupTestBed = registerTestBed<TestSubjects>(ComponentToTest, { + const setupTestBed = registerTestBed<TestSubjects>(WithAppDependencies(MappingsEditor), { memoryRouter: { wrapComponent: false, }, @@ -312,10 +364,12 @@ export const getMappingsEditorDataFactory = (onChangeHandler: jest.MockedFunctio const [arg] = mockCalls[mockCalls.length - 1]; const { isValid, validate, getData } = arg; - let isMappingsValid = isValid; + let isMappingsValid: boolean = isValid; if (isMappingsValid === undefined) { - isMappingsValid = await act(validate); + await act(async () => { + isMappingsValid = await validate(); + }); component.update(); } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx new file mode 100644 index 0000000000000..f5fab4263e9b1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; +import { uiSettingsServiceMock } from '../../../../../../../../../../src/core/public/mocks'; +import { MappingsEditorProvider } from '../../../mappings_editor_context'; +import { createKibanaReactContext } from '../../../shared_imports'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + <input + data-test-subj={props['data-test-subj'] || 'mockComboBox'} + data-currentvalue={props.selectedOptions} + onChange={async (syntheticEvent: any) => { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + <input + data-test-subj={props['data-test-subj'] || 'mockCodeEditor'} + data-currentvalue={props.value} + onChange={(e: any) => { + props.onChange(e.jsonContent); + }} + /> + ), + // Mocking EuiSuperSelect to be able to easily change its value + // with a `myWrapper.simulate('change', { target: { value: 'someValue' } })` + EuiSuperSelect: (props: any) => ( + <input + data-test-subj={props['data-test-subj'] || 'mockSuperSelect'} + value={props.valueOfSelected} + onChange={(e) => { + props.onChange(e.target.value); + }} + /> + ), + }; +}); + +jest.mock('../../../../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual( + '../../../../../../../../../../src/plugins/kibana_react/public' + ); + + const CodeEditorMock = (props: any) => ( + <input + data-test-subj={props['data-test-subj'] || 'mockCodeEditor'} + data-value={props.value} + value={props.value} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + props.onChange(e.target.value); + }} + /> + ); + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +const { GlobalFlyoutProvider } = GlobalFlyout; + +const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings: uiSettingsServiceMock.createSetupContract(), +}); + +const defaultProps = { + docLinks: { + DOC_LINK_VERSION: 'master', + ELASTIC_WEBSITE_URL: 'https://jest.elastic.co', + }, +}; + +export const WithAppDependencies = (Comp: any) => (props: any) => ( + <KibanaReactContextProvider> + <MappingsEditorProvider> + <GlobalFlyoutProvider> + <Comp {...defaultProps} {...props} /> + </GlobalFlyoutProvider> + </MappingsEditorProvider> + </KibanaReactContextProvider> +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx index 8e5a3a314c6f6..d6dcc317e67ef 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx @@ -25,7 +25,7 @@ describe('Mappings editor: mapped fields', () => { describe('<DocumentFieldsTreeEditor />', () => { let testBed: MappingsEditorTestBed; - const defaultMappings = { + let defaultMappings = { properties: { myField: { type: 'text', @@ -72,6 +72,50 @@ describe('Mappings editor: mapped fields', () => { expect(domTreeMetadata).toEqual(defaultMappings.properties); }); + test('should indicate when a field is shadowed by a runtime field', async () => { + defaultMappings = { + properties: { + // myField is shadowed by runtime field with same name + myField: { + type: 'text', + fields: { + // Same name but is not root so not shadowed + myField: { + type: 'text', + }, + }, + }, + myObject: { + type: 'object', + properties: { + // Object properties are also non root fields so not shadowed + myField: { + type: 'object', + }, + }, + }, + }, + runtime: { + myField: { + type: 'boolean', + script: { + source: 'emit("hello")', + }, + }, + }, + } as any; + + const { actions, find } = setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + + await actions.expandAllFieldsAndReturnMetadata(); + + expect(find('fieldsListItem').length).toBe(4); // 2 for text and 2 for object + expect(find('fieldsListItem.isShadowedIndicator').length).toBe(1); // only root level text field + }); + test('should allow to be controlled by parent component and update on prop change', async () => { testBed = setup({ value: defaultMappings, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index f5fcff9f96254..ead4fef5506e5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -129,6 +129,18 @@ describe('Mappings editor: core', () => { testBed.component.update(); }); + test('should have 4 tabs (fields, runtime, template, advanced settings)', () => { + const { find } = testBed; + const tabs = find('formTab').map((wrapper) => wrapper.text()); + + expect(tabs).toEqual([ + 'Mapped fields', + 'Runtime fields', + 'Dynamic templates', + 'Advanced options', + ]); + }); + test('should keep the changes when switching tabs', async () => { const { actions: { addField, selectTab, updateJsonEditor, getJsonEditorValue, getToggleValue }, @@ -196,7 +208,6 @@ describe('Mappings editor: core', () => { isNumericDetectionVisible = exists('advancedConfiguration.numericDetection'); expect(isNumericDetectionVisible).toBe(false); - // await act(() => promise); // ---------------------------------------------------------------------------- // Go back to dynamic templates tab and make sure our changes are still there // ---------------------------------------------------------------------------- diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/runtime_fields.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/runtime_fields.test.tsx new file mode 100644 index 0000000000000..dc7859c24fb9e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/runtime_fields.test.tsx @@ -0,0 +1,246 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from './helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; + +describe('Mappings editor: runtime fields', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + + describe('<RuntimeFieldsList />', () => { + let testBed: MappingsEditorTestBed; + + describe('when there are no runtime fields', () => { + const defaultMappings = {}; + + beforeEach(async () => { + testBed = setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + + await testBed.actions.selectTab('runtimeFields'); + }); + + test('should display an empty prompt', () => { + const { exists, find } = testBed; + + expect(exists('emptyList')).toBe(true); + expect(find('emptyList').text()).toContain('Start by creating a runtime field'); + }); + + test('should have a button to create a field and a link that points to the docs', () => { + const { exists, find, actions } = testBed; + + expect(exists('emptyList.learnMoreLink')).toBe(true); + expect(exists('emptyList.createRuntimeFieldButton')).toBe(true); + expect(find('createRuntimeFieldButton').text()).toBe('Create runtime field'); + + expect(exists('runtimeFieldEditor')).toBe(false); + actions.openRuntimeFieldEditor(); + expect(exists('runtimeFieldEditor')).toBe(true); + }); + }); + + describe('when there are runtime fields', () => { + const defaultMappings = { + runtime: { + day_of_week: { + type: 'date', + script: { + source: 'emit("hello Kibana")', + }, + }, + }, + }; + + beforeEach(async () => { + testBed = setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + + await testBed.actions.selectTab('runtimeFields'); + }); + + test('should list the fields', async () => { + const { find, actions } = testBed; + + const fields = actions.getRuntimeFieldsList(); + expect(fields.length).toBe(1); + + const [field] = fields; + expect(field.name).toBe('day_of_week'); + expect(field.type).toBe('Date'); + + await actions.startEditRuntimeField('day_of_week'); + expect(find('runtimeFieldEditor.scriptField').props().value).toBe('emit("hello Kibana")'); + }); + + test('should have a button to create fields', () => { + const { actions, exists } = testBed; + + expect(exists('createRuntimeFieldButton')).toBe(true); + + actions.openRuntimeFieldEditor(); + expect(exists('runtimeFieldEditor')).toBe(true); + }); + + test('should close the runtime editor when switching tab', async () => { + const { exists, actions } = testBed; + expect(exists('runtimeFieldEditor')).toBe(false); // closed + + actions.openRuntimeFieldEditor(); + expect(exists('runtimeFieldEditor')).toBe(true); // opened + + // Navigate away + await testBed.actions.selectTab('templates'); + expect(exists('runtimeFieldEditor')).toBe(false); // closed + + // Back to runtime fields + await testBed.actions.selectTab('runtimeFields'); + expect(exists('runtimeFieldEditor')).toBe(false); // still closed + }); + }); + + describe('Create / edit / delete runtime fields', () => { + const defaultMappings = {}; + + beforeEach(async () => { + testBed = setup({ + value: defaultMappings, + onChange: onChangeHandler, + }); + + await testBed.actions.selectTab('runtimeFields'); + }); + + test('should add the runtime field to the list and remove the empty prompt', async () => { + const { exists, actions, component } = testBed; + + await actions.addRuntimeField({ + name: 'myField', + script: { source: 'emit("hello")' }, + type: 'boolean', + }); + + // Make sure editor is closed and the field is in the list + expect(exists('runtimeFieldEditor')).toBe(false); + expect(exists('emptyList')).toBe(false); + + const fields = actions.getRuntimeFieldsList(); + expect(fields.length).toBe(1); + + const [field] = fields; + expect(field.name).toBe('myField'); + expect(field.type).toBe('Boolean'); + + // Make sure the field has been added to forwarded data + ({ data } = await getMappingsEditorData(component)); + + expect(data).toEqual({ + runtime: { + myField: { + type: 'boolean', + script: { + source: 'emit("hello")', + }, + }, + }, + }); + }); + + test('should remove the runtime field from the list', async () => { + const { actions, component } = testBed; + + await actions.addRuntimeField({ + name: 'myField', + script: { source: 'emit("hello")' }, + type: 'boolean', + }); + + let fields = actions.getRuntimeFieldsList(); + expect(fields.length).toBe(1); + ({ data } = await getMappingsEditorData(component)); + expect(data).toBeDefined(); + expect(data.runtime).toBeDefined(); + + await actions.deleteRuntimeField('myField'); + + fields = actions.getRuntimeFieldsList(); + expect(fields.length).toBe(0); + + ({ data } = await getMappingsEditorData(component)); + + expect(data).toBeUndefined(); + }); + + test('should edit the runtime field', async () => { + const { find, component, actions } = testBed; + + await actions.addRuntimeField({ + name: 'myField', + script: { source: 'emit("hello")' }, + type: 'boolean', + }); + + let fields = actions.getRuntimeFieldsList(); + expect(fields.length).toBe(1); + + await actions.startEditRuntimeField('myField'); + await actions.updateRuntimeFieldForm({ + name: 'updatedName', + script: { source: 'new script' }, + type: 'date', + }); + + await act(async () => { + find('runtimeFieldEditor.saveFieldButton').simulate('click'); + }); + component.update(); + + fields = actions.getRuntimeFieldsList(); + const [field] = fields; + + expect(field.name).toBe('updatedName'); + expect(field.type).toBe('Date'); + + ({ data } = await getMappingsEditorData(component)); + + expect(data).toEqual({ + runtime: { + updatedName: { + type: 'date', + script: { + source: 'new script', + }, + }, + }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx index 56c01510376be..84c4bf491cef5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx @@ -25,7 +25,7 @@ export const DocumentFieldsHeader = React.memo(({ searchValue, onSearchChange }: defaultMessage="Define the fields for your indexed documents. {docsLink}" values={{ docsLink: ( - <EuiLink href={documentationService.getMappingTypesLink()} target="_blank"> + <EuiLink href={documentationService.getMappingTypesLink()} target="_blank" external> {i18n.translate('xpack.idxMgmt.mappingsEditor.documentFieldsDocumentationLink', { defaultMessage: 'Learn more.', })} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx index 1457c4583aa0e..c613ddf282f0a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx @@ -16,7 +16,7 @@ import { SelectOption, SuperSelectOption, } from '../../../types'; -import { useIndexSettings } from '../../../index_settings_context'; +import { useConfig } from '../../../config_context'; import { AnalyzerParameterSelects } from './analyzer_parameter_selects'; interface Props { @@ -71,7 +71,9 @@ export const AnalyzerParameter = ({ allowsIndexDefaultOption = true, 'data-test-subj': dataTestSubj, }: Props) => { - const { value: indexSettings } = useIndexSettings(); + const { + value: { indexSettings }, + } = useConfig(); const customAnalyzers = getCustomAnalyzers(indexSettings); const analyzerOptions = allowsIndexDefaultOption ? ANALYZER_OPTIONS diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index c47ea4a884111..b3bf071948956 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -73,10 +73,6 @@ export * from './meta_parameter'; export * from './ignore_above_parameter'; -export { RuntimeTypeParameter } from './runtime_type_parameter'; - -export { PainlessScriptParameter } from './painless_script_parameter'; - export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; export const PARAMETER_DESERIALIZERS = [relationsDeserializer, dynamicDeserializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx deleted file mode 100644 index 9042e7f6ee328..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { PainlessLang } from '@kbn/monaco'; -import { EuiFormRow, EuiDescribedFormGroup } from '@elastic/eui'; - -import { CodeEditor, UseField } from '../../../shared_imports'; -import { getFieldConfig } from '../../../lib'; -import { EditFieldFormRow } from '../fields/edit_field'; - -interface Props { - stack?: boolean; -} - -export const PainlessScriptParameter = ({ stack }: Props) => { - return ( - <UseField<string> path="script.source" config={getFieldConfig('script')}> - {(scriptField) => { - const error = scriptField.getErrorsMessages(); - const isInvalid = error ? Boolean(error.length) : false; - - const field = ( - <EuiFormRow label={scriptField.label} error={error} isInvalid={isInvalid} fullWidth> - <CodeEditor - languageId={PainlessLang.ID} - width="100%" - height="400px" - value={scriptField.value} - onChange={scriptField.setValue} - options={{ - fontSize: 12, - minimap: { - enabled: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', - automaticLayout: true, - }} - /> - </EuiFormRow> - ); - - const fieldTitle = i18n.translate('xpack.idxMgmt.mappingsEditor.painlessScript.title', { - defaultMessage: 'Emitted value', - }); - - const fieldDescription = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.painlessScript.description', - { - defaultMessage: 'Use emit() to define the value of this runtime field.', - } - ); - - if (stack) { - return ( - <EditFieldFormRow title={fieldTitle} description={fieldDescription} withToggle={false}> - {field} - </EditFieldFormRow> - ); - } - - return ( - <EuiDescribedFormGroup - title={<h3>{fieldTitle}</h3>} - description={fieldDescription} - fullWidth={true} - > - {field} - </EuiDescribedFormGroup> - ); - }} - </UseField> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx deleted file mode 100644 index 95a6c5364ac4d..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFormRow, - EuiComboBox, - EuiComboBoxOptionOption, - EuiDescribedFormGroup, - EuiSpacer, -} from '@elastic/eui'; - -import { UseField, RUNTIME_FIELD_OPTIONS } from '../../../shared_imports'; -import { DataType } from '../../../types'; -import { getFieldConfig } from '../../../lib'; -import { TYPE_DEFINITION } from '../../../constants'; -import { EditFieldFormRow, FieldDescriptionSection } from '../fields/edit_field'; - -interface Props { - stack?: boolean; -} - -export const RuntimeTypeParameter = ({ stack }: Props) => { - return ( - <UseField<EuiComboBoxOptionOption[]> - path="runtime_type" - config={getFieldConfig('runtime_type')} - > - {(runtimeTypeField) => { - const { label, value, setValue } = runtimeTypeField; - const typeDefinition = - TYPE_DEFINITION[(value as EuiComboBoxOptionOption[])[0]!.value as DataType]; - - const field = ( - <> - <EuiFormRow label={label} fullWidth> - <EuiComboBox - placeholder={i18n.translate( - 'xpack.idxMgmt.mappingsEditor.runtimeType.placeholderLabel', - { - defaultMessage: 'Select a type', - } - )} - singleSelection={{ asPlainText: true }} - options={RUNTIME_FIELD_OPTIONS} - selectedOptions={value} - onChange={(newValue) => { - if (newValue.length === 0) { - // Don't allow clearing the type. One must always be selected - return; - } - setValue(newValue); - }} - isClearable={false} - fullWidth - /> - </EuiFormRow> - - <EuiSpacer size="m" /> - - {/* Field description */} - {typeDefinition && ( - <FieldDescriptionSection isMultiField={false}> - {typeDefinition.description?.() as JSX.Element} - </FieldDescriptionSection> - )} - </> - ); - - const fieldTitle = i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeType.title', { - defaultMessage: 'Emitted type', - }); - - const fieldDescription = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.runtimeType.description', - { - defaultMessage: 'Select the type of value emitted by the runtime field.', - } - ); - - if (stack) { - return ( - <EditFieldFormRow title={fieldTitle} description={fieldDescription} withToggle={false}> - {field} - </EditFieldFormRow> - ); - } - - return ( - <EuiDescribedFormGroup - title={<h3>{fieldTitle}</h3>} - description={fieldDescription} - fullWidth={true} - > - {field} - </EuiDescribedFormGroup> - ); - }} - </UseField> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts index 5c04b2fbb336c..ccd1312ed4896 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts @@ -11,7 +11,6 @@ import { AliasTypeRequiredParameters } from './alias_type'; import { TokenCountTypeRequiredParameters } from './token_count_type'; import { ScaledFloatTypeRequiredParameters } from './scaled_float_type'; import { DenseVectorRequiredParameters } from './dense_vector_type'; -import { RuntimeTypeRequiredParameters } from './runtime_type'; export interface ComponentProps { allFields: NormalizedFields['byId']; @@ -22,7 +21,6 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType<any> } = { token_count: TokenCountTypeRequiredParameters, scaled_float: ScaledFloatTypeRequiredParameters, dense_vector: DenseVectorRequiredParameters, - runtime: RuntimeTypeRequiredParameters, }; export const getRequiredParametersFormForType = ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx deleted file mode 100644 index 54907295f8a15..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { RuntimeTypeParameter, PainlessScriptParameter } from '../../../field_parameters'; - -export const RuntimeTypeRequiredParameters = () => { - return ( - <> - <RuntimeTypeParameter /> - <PainlessScriptParameter /> - </> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index d135d1b81419c..0f9308aa43448 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -31,7 +31,6 @@ import { JoinType } from './join_type'; import { HistogramType } from './histogram_type'; import { ConstantKeywordType } from './constant_keyword_type'; import { RankFeatureType } from './rank_feature_type'; -import { RuntimeType } from './runtime_type'; import { WildcardType } from './wildcard_type'; import { PointType } from './point_type'; import { VersionType } from './version_type'; @@ -62,7 +61,6 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType<any> } = { histogram: HistogramType, constant_keyword: ConstantKeywordType, rank_feature: RankFeatureType, - runtime: RuntimeType, wildcard: WildcardType, point: PointType, version: VersionType, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx deleted file mode 100644 index dcf5a74e0e304..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { RuntimeTypeParameter, PainlessScriptParameter } from '../../field_parameters'; -import { BasicParametersSection } from '../edit_field'; - -export const RuntimeType = () => { - return ( - <BasicParametersSection> - <RuntimeTypeParameter stack={true} /> - <PainlessScriptParameter stack={true} /> - </BasicParametersSection> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 1939f09fa6762..22898a7b2b92e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -23,6 +23,27 @@ import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; import { DeleteFieldProvider } from './delete_field_provider'; +const i18nTexts = { + addMultiFieldButtonLabel: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel', + { + defaultMessage: 'Add a multi-field to index the same field in different ways.', + } + ), + addPropertyButtonLabel: i18n.translate('xpack.idxMgmt.mappingsEditor.addPropertyButtonLabel', { + defaultMessage: 'Add property', + }), + editButtonLabel: i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldButtonLabel', { + defaultMessage: 'Edit', + }), + deleteButtonLabel: i18n.translate('xpack.idxMgmt.mappingsEditor.removeFieldButtonLabel', { + defaultMessage: 'Remove', + }), + fieldIsShadowedLabel: i18n.translate('xpack.idxMgmt.mappingsEditor.fieldIsShadowedLabel', { + defaultMessage: 'Field shadowed by a runtime field with the same name.', + }), +}; + interface Props { field: NormalizedField; allFields: NormalizedFields['byId']; @@ -31,6 +52,7 @@ interface Props { isHighlighted: boolean; isDimmed: boolean; isLastItem: boolean; + isShadowed?: boolean; childFieldsArray: NormalizedField[]; maxNestedDepth: number; addField(): void; @@ -48,6 +70,7 @@ function FieldListItemComponent( isCreateFieldFormVisible, areActionButtonsVisible, isLastItem, + isShadowed = false, childFieldsArray, maxNestedDepth, addField, @@ -106,30 +129,12 @@ function FieldListItemComponent( return null; } - const addMultiFieldButtonLabel = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel', - { - defaultMessage: 'Add a multi-field to index the same field in different ways.', - } - ); - - const addPropertyButtonLabel = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.addPropertyButtonLabel', - { - defaultMessage: 'Add property', - } - ); - - const editButtonLabel = i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldButtonLabel', { - defaultMessage: 'Edit', - }); - - const deleteButtonLabel = i18n.translate( - 'xpack.idxMgmt.mappingsEditor.removeFieldButtonLabel', - { - defaultMessage: 'Remove', - } - ); + const { + addMultiFieldButtonLabel, + addPropertyButtonLabel, + editButtonLabel, + deleteButtonLabel, + } = i18nTexts; return ( <EuiFlexGroup gutterSize="s" className="mappingsEditor__fieldsListItem__actions"> @@ -288,6 +293,18 @@ function FieldListItemComponent( </EuiBadge> </EuiFlexItem> + {isShadowed && ( + <EuiFlexItem grow={false}> + <EuiToolTip content={i18nTexts.fieldIsShadowedLabel}> + <EuiBadge color="warning" data-test-subj="isShadowedIndicator"> + {i18n.translate('xpack.idxMgmt.mappingsEditor.shadowedBadgeLabel', { + defaultMessage: 'Shadowed', + })} + </EuiBadge> + </EuiToolTip> + </EuiFlexItem> + )} + <EuiFlexItem grow={false}>{renderActionButtons()}</EuiFlexItem> </EuiFlexGroup> </div> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index 7d9ad3bc6aaec..02d915ee349b0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -20,10 +20,12 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop const listElement = useRef<HTMLLIElement | null>(null); const { documentFields: { status, fieldToAddFieldTo, fieldToEdit }, - fields: { byId, maxNestedDepth }, + fields: { byId, maxNestedDepth, rootLevelFields }, + runtimeFields, } = useMappingsState(); const getField = useCallback((id: string) => byId[id], [byId]); + const runtimeFieldNames = Object.values(runtimeFields).map((field) => field.source.name); const field: NormalizedField = getField(fieldId); const { childFields } = field; @@ -35,6 +37,10 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop () => (childFields !== undefined ? childFields.map(getField) : []), [childFields, getField] ); + // Indicate if the field is shadowed by a runtime field with the same name + // Currently this can only occur for **root level** fields. + const isShadowed = + rootLevelFields.includes(fieldId) && runtimeFieldNames.includes(field.source.name); const addField = useCallback(() => { dispatch({ @@ -62,6 +68,7 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop treeDepth={treeDepth} isHighlighted={isHighlighted} isDimmed={isDimmed} + isShadowed={isShadowed} isCreateFieldFormVisible={isCreateFieldFormVisible} areActionButtonsVisible={areActionButtonsVisible} isLastItem={isLastItem} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/index.ts index 2958ecd75910f..2a19ccb3f5d1c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/index.ts @@ -8,6 +8,8 @@ export * from './configuration_form'; export * from './document_fields'; +export * from './runtime_fields'; + export * from './templates_form'; export * from './multiple_mappings_warning'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx new file mode 100644 index 0000000000000..17daf7d671c5d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +import { useDispatch } from '../../mappings_state_context'; +import { NormalizedRuntimeField } from '../../types'; + +type DeleteFieldFunc = (property: NormalizedRuntimeField) => void; + +interface Props { + children: (deleteProperty: DeleteFieldFunc) => React.ReactNode; +} + +interface State { + isModalOpen: boolean; + field?: NormalizedRuntimeField; +} + +export const DeleteRuntimeFieldProvider = ({ children }: Props) => { + const [state, setState] = useState<State>({ isModalOpen: false }); + const dispatch = useDispatch(); + + const confirmButtonText = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.deleteRuntimeField.confirmationModal.removeButtonLabel', + { + defaultMessage: 'Remove', + } + ); + + let modalTitle: string | undefined; + + if (state.field) { + const { source } = state.field; + + modalTitle = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.deleteRuntimeField.confirmationModal.title', + { + defaultMessage: "Remove runtime field '{fieldName}'?", + values: { + fieldName: source.name, + }, + } + ); + } + + const deleteField: DeleteFieldFunc = (field) => { + setState({ isModalOpen: true, field }); + }; + + const closeModal = () => { + setState({ isModalOpen: false }); + }; + + const confirmDelete = () => { + dispatch({ type: 'runtimeField.remove', value: state.field!.id }); + closeModal(); + }; + + return ( + <> + {children(deleteField)} + + {state.isModalOpen && ( + <EuiOverlayMask> + <EuiConfirmModal + title={modalTitle} + data-test-subj="runtimeFieldDeleteConfirmModal" + onCancel={closeModal} + onConfirm={confirmDelete} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.mappingsEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + buttonColor="danger" + confirmButtonText={confirmButtonText} + /> + </EuiOverlayMask> + )} + </> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/empty_prompt.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/empty_prompt.tsx new file mode 100644 index 0000000000000..7fb2b9d7df967 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/empty_prompt.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; + +interface Props { + createField: () => void; + runtimeFieldsDocsUri: string; +} + +export const EmptyPrompt: FunctionComponent<Props> = ({ createField, runtimeFieldsDocsUri }) => { + return ( + <EuiEmptyPrompt + iconType="managementApp" + data-test-subj="emptyList" + title={ + <h2 data-test-subj="title"> + {i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeFields.emptyPromptTitle', { + defaultMessage: 'Start by creating a runtime field', + })} + </h2> + } + body={ + <p> + <FormattedMessage + id="xpack.idxMgmt.mappingsEditor.runtimeFields.emptyPromptDescription" + defaultMessage="Define a field in the mapping and evaluate it at search time." + /> + <br /> + <EuiLink + href={runtimeFieldsDocsUri} + target="_blank" + data-test-subj="learnMoreLink" + external + > + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.runtimeFields.emptyPromptDocumentionLink', + { + defaultMessage: 'Learn more.', + } + )} + </EuiLink> + </p> + } + actions={ + <EuiButton + onClick={() => createField()} + iconType="plusInCircle" + data-test-subj="createRuntimeFieldButton" + fill + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeFields.emptyPromptButtonLabel', { + defaultMessage: 'Create runtime field', + })} + </EuiButton> + } + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/index.ts new file mode 100644 index 0000000000000..e5928ebb07ddc --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/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 { RuntimeFieldsList } from './runtime_fields_list'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx new file mode 100644 index 0000000000000..dce5ad1657d38 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx @@ -0,0 +1,151 @@ +/* + * 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, { useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiButtonEmpty, EuiText, EuiLink } from '@elastic/eui'; + +import { useMappingsState, useDispatch } from '../../mappings_state_context'; +import { + documentationService, + GlobalFlyout, + RuntimeField, + RuntimeFieldEditorFlyoutContent, + RuntimeFieldEditorFlyoutContentProps, +} from '../../shared_imports'; +import { useConfig } from '../../config_context'; +import { EmptyPrompt } from './empty_prompt'; +import { RuntimeFieldsListItemContainer } from './runtimefields_list_item_container'; + +const { useGlobalFlyout } = GlobalFlyout; + +export const RuntimeFieldsList = () => { + const runtimeFieldsDocsUri = documentationService.getRuntimeFields(); + const { + runtimeFields, + runtimeFieldsList: { status, fieldToEdit }, + fields, + } = useMappingsState(); + + const dispatch = useDispatch(); + + const { + addContent: addContentToGlobalFlyout, + removeContent: removeContentFromGlobalFlyout, + } = useGlobalFlyout(); + + const { + value: { docLinks }, + } = useConfig(); + + const createField = useCallback(() => { + dispatch({ type: 'runtimeFieldsList.createField' }); + }, [dispatch]); + + const exitEdit = useCallback(() => { + dispatch({ type: 'runtimeFieldsList.closeRuntimeFieldEditor' }); + }, [dispatch]); + + const saveRuntimeField = useCallback( + (field: RuntimeField) => { + if (fieldToEdit) { + dispatch({ + type: 'runtimeField.edit', + value: { + id: fieldToEdit, + source: field, + }, + }); + } else { + dispatch({ type: 'runtimeField.add', value: field }); + } + }, + [dispatch, fieldToEdit] + ); + + useEffect(() => { + if (status === 'creatingField' || status === 'editingField') { + addContentToGlobalFlyout<RuntimeFieldEditorFlyoutContentProps>({ + id: 'runtimeFieldEditor', + Component: RuntimeFieldEditorFlyoutContent, + props: { + onSave: saveRuntimeField, + onCancel: exitEdit, + defaultValue: fieldToEdit ? runtimeFields[fieldToEdit]?.source : undefined, + docLinks: docLinks!, + ctx: { + namesNotAllowed: Object.values(runtimeFields).map((field) => field.source.name), + existingConcreteFields: Object.values(fields.byId).map((field) => field.source.name), + }, + }, + flyoutProps: { + 'data-test-subj': 'runtimeFieldEditor', + 'aria-labelledby': 'runtimeFieldEditorEditTitle', + maxWidth: 720, + onClose: exitEdit, + }, + cleanUpFunc: exitEdit, + }); + } else if (status === 'idle') { + removeContentFromGlobalFlyout('runtimeFieldEditor'); + } + }, [ + status, + fieldToEdit, + runtimeFields, + fields, + docLinks, + addContentToGlobalFlyout, + removeContentFromGlobalFlyout, + saveRuntimeField, + exitEdit, + ]); + + const fieldsToArray = Object.entries(runtimeFields); + const isEmpty = fieldsToArray.length === 0; + const isCreateFieldDisabled = status !== 'idle'; + + return isEmpty ? ( + <EmptyPrompt createField={createField} runtimeFieldsDocsUri={runtimeFieldsDocsUri} /> + ) : ( + <> + <EuiText size="s" color="subdued"> + <FormattedMessage + id="xpack.idxMgmt.mappingsEditor.runtimeFieldsDescription" + defaultMessage="Define the runtime fields accessible at search time. {docsLink}" + values={{ + docsLink: ( + <EuiLink href={runtimeFieldsDocsUri} target="_blank" external> + {i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeFieldsDocumentationLink', { + defaultMessage: 'Learn more.', + })} + </EuiLink> + ), + }} + /> + </EuiText> + <EuiSpacer /> + <ul> + {fieldsToArray.map(([fieldId]) => ( + <RuntimeFieldsListItemContainer key={fieldId} fieldId={fieldId} /> + ))} + </ul> + + <EuiSpacer /> + + <EuiButtonEmpty + disabled={isCreateFieldDisabled} + onClick={createField} + iconType="plusInCircleFilled" + data-test-subj="createRuntimeFieldButton" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.addRuntimeFieldButtonLabel', { + defaultMessage: 'Add field', + })} + </EuiButtonEmpty> + </> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item.tsx new file mode 100644 index 0000000000000..754004ae0c622 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import classNames from 'classnames'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { NormalizedRuntimeField } from '../../types'; +import { getTypeLabelFromField } from '../../lib'; + +import { DeleteRuntimeFieldProvider } from './delete_field_provider'; + +interface Props { + field: NormalizedRuntimeField; + areActionButtonsVisible: boolean; + isHighlighted: boolean; + isDimmed: boolean; + editField(): void; +} + +function RuntimeFieldsListItemComponent( + { field, areActionButtonsVisible, isHighlighted, isDimmed, editField }: Props, + ref: React.Ref<HTMLLIElement> +) { + const { source } = field; + + const renderActionButtons = () => { + if (!areActionButtonsVisible) { + return null; + } + + const editButtonLabel = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.editRuntimeFieldButtonLabel', + { + defaultMessage: 'Edit', + } + ); + + const deleteButtonLabel = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.removeRuntimeFieldButtonLabel', + { + defaultMessage: 'Remove', + } + ); + + return ( + <EuiFlexGroup gutterSize="s" className="mappingsEditor__fieldsListItem__actions"> + <EuiFlexItem grow={false}> + <EuiToolTip content={editButtonLabel}> + <EuiButtonIcon + iconType="pencil" + onClick={editField} + data-test-subj="editFieldButton" + aria-label={editButtonLabel} + /> + </EuiToolTip> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <DeleteRuntimeFieldProvider> + {(deleteField) => ( + <EuiToolTip content={deleteButtonLabel}> + <EuiButtonIcon + iconType="trash" + color="danger" + onClick={() => deleteField(field)} + data-test-subj="removeFieldButton" + aria-label={deleteButtonLabel} + /> + </EuiToolTip> + )} + </DeleteRuntimeFieldProvider> + </EuiFlexItem> + </EuiFlexGroup> + ); + }; + + return ( + <li className="mappingsEditor__fieldsListItem" data-test-subj="runtimeFieldsListItem"> + <div + className={classNames('mappingsEditor__fieldsListItem__field', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'mappingsEditor__fieldsListItem__field--enabled': areActionButtonsVisible, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'mappingsEditor__fieldsListItem__field--highlighted': isHighlighted, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'mappingsEditor__fieldsListItem__field--dim': isDimmed, + })} + > + <div className="mappingsEditor__fieldsListItem__wrapper mappingsEditor__fieldsListItem__wrapper--indent"> + <EuiFlexGroup + gutterSize="s" + alignItems="center" + className="mappingsEditor__fieldsListItem__content" + > + <EuiFlexItem + grow={false} + className="mappingsEditor__fieldsListItem__name" + data-test-subj="fieldName" + > + {source.name} + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiBadge color="hollow" data-test-subj="fieldType" data-type-value={source.type}> + {getTypeLabelFromField(source)} + </EuiBadge> + </EuiFlexItem> + + <EuiFlexItem grow={false}>{renderActionButtons()}</EuiFlexItem> + </EuiFlexGroup> + </div> + </div> + </li> + ); +} + +export const RuntimeFieldsListItem = React.memo( + RuntimeFieldsListItemComponent +) as typeof RuntimeFieldsListItemComponent; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item_container.tsx new file mode 100644 index 0000000000000..90008193fa056 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtimefields_list_item_container.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; + +import { useMappingsState, useDispatch } from '../../mappings_state_context'; +import { NormalizedRuntimeField } from '../../types'; +import { RuntimeFieldsListItem } from './runtimefields_list_item'; + +interface Props { + fieldId: string; +} + +export const RuntimeFieldsListItemContainer = ({ fieldId }: Props) => { + const dispatch = useDispatch(); + const { + runtimeFieldsList: { status, fieldToEdit }, + runtimeFields, + } = useMappingsState(); + + const getField = useCallback((id: string) => runtimeFields[id], [runtimeFields]); + + const field: NormalizedRuntimeField = getField(fieldId); + const isHighlighted = fieldToEdit === fieldId; + const isDimmed = status === 'editingField' && fieldToEdit !== fieldId; + const areActionButtonsVisible = status === 'idle'; + + const editField = useCallback(() => { + dispatch({ + type: 'runtimeFieldsList.editField', + value: fieldId, + }); + }, [fieldId, dispatch]); + + return ( + <RuntimeFieldsListItem + field={field} + isHighlighted={isHighlighted} + isDimmed={isDimmed} + areActionButtonsVisible={areActionButtonsVisible} + editField={editField} + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/config_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/config_context.tsx new file mode 100644 index 0000000000000..84b42508f904a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/config_context.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { createContext, useContext, useState } from 'react'; + +import { DocLinksStart } from './shared_imports'; +import { IndexSettings } from './types'; + +interface ContextState { + indexSettings: IndexSettings; + docLinks?: DocLinksStart; +} + +interface Context { + value: ContextState; + update: (value: ContextState) => void; +} + +const ConfigContext = createContext<Context | undefined>(undefined); + +interface Props { + children: React.ReactNode; +} + +export const ConfigProvider = ({ children }: Props) => { + const [state, setState] = useState<ContextState>({ + indexSettings: {}, + }); + + return ( + <ConfigContext.Provider value={{ value: state, update: setState }}> + {children} + </ConfigContext.Provider> + ); +}; + +export const useConfig = () => { + const ctx = useContext(ConfigContext); + if (ctx === undefined) { + throw new Error('useConfig must be used within a <ConfigProvider />'); + } + return ctx; +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 07ca0a69afefb..66be208fbb66b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -13,25 +13,6 @@ import { documentationService } from '../../../services/documentation'; import { MainType, SubType, DataType, DataTypeDefinition } from '../types'; export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { - runtime: { - value: 'runtime', - isBeta: true, - label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.runtimeFieldDescription', { - defaultMessage: 'Runtime', - }), - // TODO: Add this once the page exists. - // documentation: { - // main: '/runtime_field.html', - // }, - description: () => ( - <p> - <FormattedMessage - id="xpack.idxMgmt.mappingsEditor.dataType.runtimeFieldLongDescription" - defaultMessage="Runtime fields define scripts that calculate field values at runtime." - /> - </p> - ), - }, text: { value: 'text', label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.textDescription', { @@ -944,7 +925,6 @@ export const MAIN_TYPES: MainType[] = [ 'range', 'rank_feature', 'rank_features', - 'runtime', 'search_as_you_type', 'shape', 'text', diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx index 46292b7b2d357..d16bf68b80e5d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx @@ -18,7 +18,6 @@ export const TYPE_NOT_ALLOWED_MULTIFIELD: DataType[] = [ 'object', 'nested', 'alias', - 'runtime', ]; export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index 64f84ee2611a0..281b14a25fcb6 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -16,8 +16,6 @@ import { ValidationFuncArg, fieldFormatters, FieldConfig, - RUNTIME_FIELD_OPTIONS, - RuntimeType, } from '../shared_imports'; import { AliasOption, @@ -187,52 +185,6 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, schema: t.string, }, - runtime_type: { - fieldConfig: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.runtimeTypeLabel', { - defaultMessage: 'Type', - }), - defaultValue: 'keyword', - deserializer: (fieldType: RuntimeType | undefined) => { - if (typeof fieldType === 'string' && Boolean(fieldType)) { - const label = - RUNTIME_FIELD_OPTIONS.find(({ value }) => value === fieldType)?.label ?? fieldType; - return [ - { - label, - value: fieldType, - }, - ]; - } - return []; - }, - serializer: (value: ComboBoxOption[]) => (value.length === 0 ? '' : value[0].value), - }, - schema: t.string, - }, - script: { - fieldConfig: { - defaultValue: '', - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.painlessScriptLabel', { - defaultMessage: 'Script', - }), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.idxMgmt.mappingsEditor.parameters.validations.scriptIsRequiredErrorMessage', - { - defaultMessage: 'Script must emit() a value.', - } - ) - ), - }, - ], - }, - schema: t.string, - }, store: { fieldConfig: { type: FIELD_TYPES.CHECKBOX, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx deleted file mode 100644 index bd84c3a905ec8..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { createContext, useContext, useState } from 'react'; - -import { IndexSettings } from './types'; - -const IndexSettingsContext = createContext< - { value: IndexSettings; update: (value: IndexSettings) => void } | undefined ->(undefined); - -interface Props { - children: React.ReactNode; -} - -export const IndexSettingsProvider = ({ children }: Props) => { - const [state, setState] = useState<IndexSettings>({}); - - return ( - <IndexSettingsContext.Provider value={{ value: state, update: setState }}> - {children} - </IndexSettingsContext.Provider> - ); -}; - -export const useIndexSettings = () => { - const ctx = useContext(IndexSettingsContext); - if (ctx === undefined) { - throw new Error('useIndexSettings must be used within a <IndexSettingsProvider />'); - } - return ctx; -}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts index 1fd8329ae4b40..c32c0d4363219 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts @@ -15,16 +15,22 @@ const isMappingDefinition = (obj: GenericObject): boolean => { return false; } - const { properties, dynamic_templates: dynamicTemplates, ...mappingsConfiguration } = obj; + const { + properties, + dynamic_templates: dynamicTemplates, + runtime, + ...mappingsConfiguration + } = obj; const { errors } = validateMappingsConfiguration(mappingsConfiguration); const isConfigurationValid = errors.length === 0; const isPropertiesValid = properties === undefined || isPlainObject(properties); const isDynamicTemplatesValid = dynamicTemplates === undefined || Array.isArray(dynamicTemplates); + const isRuntimeValid = runtime === undefined || isPlainObject(runtime); - // If the configuration, the properties and the dynamic templates are valid + // If the configuration, the properties, the dynamic templates and runtime are valid // we can assume that the mapping is declared at root level (no types) - return isConfigurationValid && isPropertiesValid && isDynamicTemplatesValid; + return isConfigurationValid && isPropertiesValid && isDynamicTemplatesValid && isRuntimeValid; }; /** diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts index 0a59cafdcef47..2a0b39c4e2c9c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts @@ -24,6 +24,8 @@ export { shouldDeleteChildFieldsAfterTypeChange, canUseMappingsEditor, stripUndefinedValues, + normalizeRuntimeFields, + deNormalizeRuntimeFields, } from './utils'; export * from './serializers'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts index f0d90be9472f6..4d1ae627bc910 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts @@ -303,4 +303,5 @@ export const VALID_MAPPINGS_PARAMETERS = [ ...mappingsConfigurationSchemaKeys, 'dynamic_templates', 'properties', + 'runtime', ]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index e1988c071314e..41ec4887a7abd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -58,10 +58,9 @@ describe('utils', () => { }); describe('getTypeLabelFromField()', () => { - test('returns an unprocessed label for non-runtime fields', () => { + test('returns label for fields', () => { expect( getTypeLabelFromField({ - name: 'testField', type: 'keyword', }) ).toBe('Keyword'); @@ -76,26 +75,5 @@ describe('utils', () => { }) ).toBe('Other: hyperdrive'); }); - - test("returns a label prepended with 'Runtime' for runtime fields", () => { - expect( - getTypeLabelFromField({ - name: 'testField', - type: 'runtime', - runtime_type: 'keyword', - }) - ).toBe('Runtime Keyword'); - }); - - test("returns a label prepended with 'Runtime Other' for unrecognized runtime fields", () => { - expect( - getTypeLabelFromField({ - name: 'testField', - type: 'runtime', - // @ts-ignore - runtime_type: 'hyperdrive', - }) - ).toBe('Runtime Other: hyperdrive'); - }); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index fd7aa41638505..283ca83c54bb0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -18,6 +18,8 @@ import { ParameterName, ComboBoxOption, GenericObject, + RuntimeFields, + NormalizedRuntimeFields, } from '../types'; import { @@ -77,15 +79,10 @@ const getTypeLabel = (type?: DataType): string => { : `${TYPE_DEFINITION.other.label}: ${type}`; }; -export const getTypeLabelFromField = (field: Field) => { - const { type, runtime_type: runtimeType } = field; +export const getTypeLabelFromField = (field: { type: DataType }) => { + const { type } = field; const typeLabel = getTypeLabel(type); - if (type === 'runtime') { - const runtimeTypeLabel = getTypeLabel(runtimeType); - return `${typeLabel} ${runtimeTypeLabel}`; - } - return typeLabel; }; @@ -566,3 +563,29 @@ export const stripUndefinedValues = <T = GenericObject>(obj: GenericObject, recu ? { ...acc, [key]: stripUndefinedValues(value, recursive) } : { ...acc, [key]: value }; }, {} as T); + +export const normalizeRuntimeFields = (fields: RuntimeFields = {}): NormalizedRuntimeFields => { + return Object.entries(fields).reduce((acc, [name, field]) => { + const id = getUniqueId(); + return { + ...acc, + [id]: { + id, + source: { + name, + ...field, + }, + }, + }; + }, {} as NormalizedRuntimeFields); +}; + +export const deNormalizeRuntimeFields = (fields: NormalizedRuntimeFields): RuntimeFields => { + return Object.values(fields).reduce((acc, { source }) => { + const { name, ...rest } = source; + return { + ...acc, + [name]: rest, + }; + }, {} as RuntimeFields); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 3902337f28ad2..3af9f24f48ed2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -9,9 +9,10 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; import { - ConfigurationForm, DocumentFields, + RuntimeFieldsList, TemplatesForm, + ConfigurationForm, MultipleMappingsWarning, } from './components'; import { @@ -21,19 +22,22 @@ import { Mappings, MappingsConfiguration, MappingsTemplates, + RuntimeFields, } from './types'; import { extractMappingsDefinition } from './lib'; import { useMappingsState } from './mappings_state_context'; import { useMappingsStateListener } from './use_state_listener'; -import { useIndexSettings } from './index_settings_context'; +import { useConfig } from './config_context'; +import { DocLinksStart } from './shared_imports'; -type TabName = 'fields' | 'advanced' | 'templates'; +type TabName = 'fields' | 'runtimeFields' | 'advanced' | 'templates'; interface MappingsEditorParsedMetadata { parsedDefaultValue?: { configuration: MappingsConfiguration; fields: { [key: string]: Field }; templates: MappingsTemplates; + runtime: RuntimeFields; }; multipleMappingsDeclared: boolean; } @@ -42,9 +46,10 @@ interface Props { onChange: OnUpdateHandler; value?: { [key: string]: any }; indexSettings?: IndexSettings; + docLinks: DocLinksStart; } -export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Props) => { +export const MappingsEditor = React.memo(({ onChange, value, docLinks, indexSettings }: Props) => { const { parsedDefaultValue, multipleMappingsDeclared, @@ -60,11 +65,12 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr _meta, _routing, dynamic, + properties, + runtime, /* eslint-disable @typescript-eslint/naming-convention */ numeric_detection, date_detection, dynamic_date_formats, - properties, dynamic_templates, /* eslint-enable @typescript-eslint/naming-convention */ } = mappingsDefinition; @@ -83,6 +89,7 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr templates: { dynamic_templates, }, + runtime, }; return { parsedDefaultValue: parsed, multipleMappingsDeclared: false }; @@ -95,12 +102,7 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr */ useMappingsStateListener({ onChange, value: parsedDefaultValue }); - // Update the Index settings context so it is available in the Global flyout - const { update: updateIndexSettings } = useIndexSettings(); - if (indexSettings !== undefined) { - updateIndexSettings(indexSettings); - } - + const { update: updateConfig } = useConfig(); const state = useMappingsState(); const [selectedTab, selectTab] = useState<TabName>('fields'); @@ -115,6 +117,14 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr } }, [multipleMappingsDeclared, onChange, value]); + useEffect(() => { + // Update the the config context so it is available globally (e.g in our Global flyout) + updateConfig({ + docLinks, + indexSettings: indexSettings ?? {}, + }); + }, [updateConfig, docLinks, indexSettings]); + const changeTab = async (tab: TabName) => { if (selectedTab === 'advanced') { // When we navigate away we need to submit the form to validate if there are any errors. @@ -139,6 +149,7 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr const tabToContentMap = { fields: <DocumentFields />, + runtimeFields: <RuntimeFieldsList />, templates: <TemplatesForm value={state.templates.defaultValue} />, advanced: <ConfigurationForm value={state.configuration.defaultValue} />, }; @@ -159,6 +170,15 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr defaultMessage: 'Mapped fields', })} </EuiTab> + <EuiTab + onClick={() => changeTab('runtimeFields')} + isSelected={selectedTab === 'runtimeFields'} + data-test-subj="formTab" + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeFieldsTabLabel', { + defaultMessage: 'Runtime fields', + })} + </EuiTab> <EuiTab onClick={() => changeTab('templates')} isSelected={selectedTab === 'templates'} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx index 8e30d07c2262f..f4d827b631dd1 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { StateProvider } from './mappings_state_context'; -import { IndexSettingsProvider } from './index_settings_context'; +import { ConfigProvider } from './config_context'; export const MappingsEditorProvider: React.FC = ({ children }) => { return ( <StateProvider> - <IndexSettingsProvider>{children}</IndexSettingsProvider> + <ConfigProvider>{children}</ConfigProvider> </StateProvider> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx index 4912b0963bc12..57c326b121141 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx @@ -41,6 +41,10 @@ export const StateProvider: React.FC = ({ children }) => { status: 'idle', editor: 'default', }, + runtimeFields: {}, + runtimeFieldsList: { + status: 'idle', + }, fieldsJsonEditor: { format: () => ({}), isValid: true, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 47e9d5200ea08..b76541479f68c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -195,6 +195,10 @@ export const reducer = (state: State, action: Action): State => { fieldToAddFieldTo: undefined, fieldToEdit: undefined, }, + runtimeFields: action.value.runtimeFields, + runtimeFieldsList: { + status: 'idle', + }, search: { term: '', result: [], @@ -482,6 +486,80 @@ export const reducer = (state: State, action: Action): State => { }, }; } + case 'runtimeFieldsList.createField': { + return { + ...state, + runtimeFieldsList: { + ...state.runtimeFieldsList, + status: 'creatingField', + }, + }; + } + case 'runtimeFieldsList.editField': { + return { + ...state, + runtimeFieldsList: { + ...state.runtimeFieldsList, + status: 'editingField', + fieldToEdit: action.value, + }, + }; + } + case 'runtimeField.add': { + const id = getUniqueId(); + const normalizedField = { + id, + source: action.value, + }; + + return { + ...state, + runtimeFields: { + ...state.runtimeFields, + [id]: normalizedField, + }, + runtimeFieldsList: { + ...state.runtimeFieldsList, + status: 'idle', + }, + }; + } + case 'runtimeField.edit': { + const fieldToEdit = state.runtimeFieldsList.fieldToEdit!; + + return { + ...state, + runtimeFields: { + ...state.runtimeFields, + [fieldToEdit]: action.value, + }, + runtimeFieldsList: { + ...state.runtimeFieldsList, + status: 'idle', + }, + }; + } + case 'runtimeField.remove': { + const field = state.runtimeFields[action.value]; + const { id } = field; + + const updatedFields = { ...state.runtimeFields }; + delete updatedFields[id]; + + return { + ...state, + runtimeFields: updatedFields, + }; + } + case 'runtimeFieldsList.closeRuntimeFieldEditor': + return { + ...state, + runtimeFieldsList: { + ...state.runtimeFieldsList, + status: 'idle', + fieldToEdit: undefined, + }, + }; case 'fieldsJsonEditor.update': { const nextState = { ...state, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 68b40e876f655..36f7fecbcff21 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -52,6 +52,14 @@ export { GlobalFlyout, } from '../../../../../../../src/plugins/es_ui_shared/public'; -export { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; +export { documentationService } from '../../services/documentation'; -export { RUNTIME_FIELD_OPTIONS, RuntimeType } from '../../../../../runtime_fields/public'; +export { + RuntimeField, + RuntimeFieldEditorFlyoutContent, + RuntimeFieldEditorFlyoutContentProps, +} from '../../../../../runtime_fields/public'; + +export { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; + +export { DocLinksStart } from '../../../../../../../src/core/public'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index b143eedd4f9d4..b5c61594e5cb0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -7,7 +7,7 @@ import { ReactNode } from 'react'; import { GenericObject } from './mappings_editor'; -import { FieldConfig } from '../shared_imports'; +import { FieldConfig, RuntimeField } from '../shared_imports'; import { PARAMETERS_DEFINITION } from '../constants'; export interface DataTypeDefinition { @@ -36,7 +36,6 @@ export interface ParameterDefinition { } export type MainType = - | 'runtime' | 'text' | 'keyword' | 'numeric' @@ -154,8 +153,6 @@ export type ParameterName = | 'depth_limit' | 'relations' | 'max_shingle_size' - | 'runtime_type' - | 'script' | 'value' | 'meta'; @@ -173,7 +170,6 @@ export interface Fields { interface FieldBasic { name: string; type: DataType; - runtime_type?: DataType; subType?: SubType; properties?: { [key: string]: Omit<Field, 'name'> }; fields?: { [key: string]: Omit<Field, 'name'> }; @@ -223,3 +219,16 @@ export interface AliasOption { id: string; label: string; } + +export interface RuntimeFields { + [name: string]: Omit<RuntimeField, 'name'>; +} + +export interface NormalizedRuntimeField { + id: string; + source: RuntimeField; +} + +export interface NormalizedRuntimeFields { + [id: string]: NormalizedRuntimeField; +} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts index 34df70374aa88..7371e348fd51c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FormHook, OnFormUpdateArg } from '../shared_imports'; -import { Field, NormalizedFields } from './document_fields'; +import { FormHook, OnFormUpdateArg, RuntimeField } from '../shared_imports'; +import { + Field, + NormalizedFields, + NormalizedRuntimeField, + NormalizedRuntimeFields, +} from './document_fields'; import { FieldsEditor, SearchResult } from './mappings_editor'; export type Mappings = MappingsTemplates & @@ -58,6 +63,11 @@ export interface DocumentFieldsState { fieldToAddFieldTo?: string; } +interface RuntimeFieldsListState { + status: DocumentFieldsStatus; + fieldToEdit?: string; +} + export interface ConfigurationFormState extends OnFormUpdateArg<MappingsConfiguration> { defaultValue: MappingsConfiguration; submitForm?: FormHook<MappingsConfiguration>['submit']; @@ -72,7 +82,9 @@ export interface State { isValid: boolean | undefined; configuration: ConfigurationFormState; documentFields: DocumentFieldsState; + runtimeFieldsList: RuntimeFieldsListState; fields: NormalizedFields; + runtimeFields: NormalizedRuntimeFields; fieldForm?: OnFormUpdateArg<any>; fieldsJsonEditor: { format(): MappingsFields; @@ -100,6 +112,12 @@ export type Action = | { type: 'documentField.editField'; value: string } | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus } | { type: 'documentField.changeEditor'; value: FieldsEditor } + | { type: 'runtimeFieldsList.createField' } + | { type: 'runtimeFieldsList.editField'; value: string } + | { type: 'runtimeFieldsList.closeRuntimeFieldEditor' } + | { type: 'runtimeField.add'; value: RuntimeField } + | { type: 'runtimeField.remove'; value: string } + | { type: 'runtimeField.edit'; value: NormalizedRuntimeField } | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } | { type: 'search:update'; value: string } | { type: 'validity:update'; value: boolean }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx index 8d039475f9cf8..79dec9cedaf7a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx @@ -11,8 +11,15 @@ import { MappingsConfiguration, MappingsTemplates, OnUpdateHandler, + RuntimeFields, } from './types'; -import { normalize, deNormalize, stripUndefinedValues } from './lib'; +import { + normalize, + deNormalize, + stripUndefinedValues, + normalizeRuntimeFields, + deNormalizeRuntimeFields, +} from './lib'; import { useMappingsState, useDispatch } from './mappings_state_context'; interface Args { @@ -21,6 +28,7 @@ interface Args { templates: MappingsTemplates; configuration: MappingsConfiguration; fields: { [key: string]: Field }; + runtime: RuntimeFields; }; } @@ -28,7 +36,13 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { const state = useMappingsState(); const dispatch = useDispatch(); - const parsedFieldsDefaultValue = useMemo(() => normalize(value?.fields), [value?.fields]); + const { fields: mappedFields, runtime: runtimeFields } = value ?? {}; + + const parsedFieldsDefaultValue = useMemo(() => normalize(mappedFields), [mappedFields]); + const parsedRuntimeFieldsDefaultValue = useMemo(() => normalizeRuntimeFields(runtimeFields), [ + runtimeFields, + ]); + useEffect(() => { // If we are creating a new field, but haven't entered any name // it is valid and we can byPass its form validation (that requires a "name" to be defined) @@ -50,6 +64,9 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { ? state.fieldsJsonEditor.format() : deNormalize(state.fields); + // Get the runtime fields + const runtime = deNormalizeRuntimeFields(state.runtimeFields); + const configurationData = state.configuration.data.format(); const templatesData = state.templates.data.format(); @@ -60,10 +77,16 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { }), }; + // Mapped fields if (fields && Object.keys(fields).length > 0) { output.properties = fields; } + // Runtime fields + if (runtime && Object.keys(runtime).length > 0) { + output.runtime = runtime; + } + return Object.keys(output).length > 0 ? (output as Mappings) : undefined; }, validate: async () => { @@ -118,7 +141,8 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { status: parsedFieldsDefaultValue.rootLevelFields.length === 0 ? 'creatingField' : 'idle', editor: 'default', }, + runtimeFields: parsedRuntimeFieldsDefaultValue, }, }); - }, [value, parsedFieldsDefaultValue, dispatch]); + }, [value, parsedFieldsDefaultValue, dispatch, parsedRuntimeFieldsDefaultValue]); }; diff --git a/x-pack/plugins/index_management/public/application/components/section_error.tsx b/x-pack/plugins/index_management/public/application/components/section_error.tsx index f807ef45559f1..86acb7bf7419a 100644 --- a/x-pack/plugins/index_management/public/application/components/section_error.tsx +++ b/x-pack/plugins/index_management/public/application/components/section_error.tsx @@ -11,6 +11,9 @@ export interface Error { cause?: string[]; message?: string; statusText?: string; + attributes?: { + cause: string[]; + }; } interface Props { @@ -20,11 +23,14 @@ interface Props { export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...rest }) => { const { - cause, // wrapEsError() on the server adds a "cause" array + cause: causeRoot, // wrapEsError() on the server adds a "cause" array message, statusText, + attributes: { cause: causeAttributes } = {}, } = error; + const cause = causeAttributes ?? causeRoot; + return ( <EuiCallOut title={title} color="danger" iconType="alert" {...rest}> <div>{message || statusText}</div> diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx index bbf7a04080a28..aeb4eb793cde8 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/eui'; import { Forms } from '../../../../../shared_imports'; +import { useAppContext } from '../../../../app_context'; import { MappingsEditor, OnUpdateHandler, @@ -33,6 +34,7 @@ interface Props { export const StepMappings: React.FunctionComponent<Props> = React.memo( ({ defaultValue = {}, onChange, indexSettings, esDocsBase }) => { const [mappings, setMappings] = useState(defaultValue); + const { docLinks } = useAppContext(); const onMappingsEditorUpdate = useCallback<OnUpdateHandler>( ({ isValid, getData, validate }) => { @@ -107,6 +109,7 @@ export const StepMappings: React.FunctionComponent<Props> = React.memo( value={mappings} onChange={onMappingsEditorUpdate} indexSettings={indexSettings} + docLinks={docLinks} /> <EuiSpacer size="m" /> diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 13e25f6d29a14..f3084630934c4 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -64,6 +64,7 @@ export async function mountManagementSection( setBreadcrumbs, uiSettings, urlGenerators, + docLinks, }; const unmountAppCallback = renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index c52b958d94ae1..e0aac742499be 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -211,6 +211,10 @@ class DocumentationService { return `${this.esDocsBase}/enabled.html`; } + public getRuntimeFields() { + return `${this.esDocsBase}/runtime.html`; + } + public getWellKnownTextLink() { return 'http://docs.opengeospatial.org/is/12-063r5/12-063r5.html'; } diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 3d70140fa60b7..99facacacfe4c 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -24,7 +24,7 @@ import { PLUGIN } from '../common'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { License, IndexDataEnricher } from './services'; -import { isEsError, handleEsError } from './shared_imports'; +import { isEsError, handleEsError, parseEsError } from './shared_imports'; import { elasticsearchJsPlugin } from './client/elasticsearch'; export interface DataManagementContext { @@ -110,6 +110,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup, indexDataEnricher: this.indexDataEnricher, lib: { isEsError, + parseEsError, handleEsError, }, }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts index b1eb430ecd2c3..656bbcc45a3b8 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts @@ -54,6 +54,7 @@ describe('GET privileges', () => { indexDataEnricher: mockedIndexDataEnricher, lib: { isEsError: jest.fn(), + parseEsError: jest.fn(), handleEsError: jest.fn(), }, }); @@ -124,6 +125,7 @@ describe('GET privileges', () => { indexDataEnricher: mockedIndexDataEnricher, lib: { isEsError: jest.fn(), + parseEsError: jest.fn(), handleEsError: jest.fn(), }, }); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 4b735c941be70..46004c64d158d 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -51,9 +51,13 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) return res.ok({ body: response }); } catch (e) { if (lib.isEsError(e)) { + const error = lib.parseEsError(e.response); return res.customError({ statusCode: e.statusCode, - body: e, + body: { + message: error.message, + attributes: error, + }, }); } // Case: default diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts index 9d078e135fd52..322f15914b735 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -29,9 +29,13 @@ export function registerSimulateRoute({ router, license, lib }: RouteDependencie return res.ok({ body: templatePreview }); } catch (e) { if (lib.isEsError(e)) { + const error = lib.parseEsError(e.response); return res.customError({ statusCode: e.statusCode, - body: e, + body: { + message: error.message, + attributes: error, + }, }); } // Case: default diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index 3055321d6b594..9ad751023db91 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -44,9 +44,13 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) return res.ok({ body: response }); } catch (e) { if (lib.isEsError(e)) { + const error = lib.parseEsError(e.response); return res.customError({ statusCode: e.statusCode, - body: e, + body: { + message: error.message, + attributes: error, + }, }); } // Case: default diff --git a/x-pack/plugins/index_management/server/shared_imports.ts b/x-pack/plugins/index_management/server/shared_imports.ts index 0606f474897b5..f7b513a8a240c 100644 --- a/x-pack/plugins/index_management/server/shared_imports.ts +++ b/x-pack/plugins/index_management/server/shared_imports.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isEsError, handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { + isEsError, + parseEsError, + handleEsError, +} from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index 177dedeb87bb4..16a6b43af8512 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -8,7 +8,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { License, IndexDataEnricher } from './services'; -import { isEsError, handleEsError } from './shared_imports'; +import { isEsError, parseEsError, handleEsError } from './shared_imports'; export interface Dependencies { security: SecurityPluginSetup; @@ -25,6 +25,7 @@ export interface RouteDependencies { indexDataEnricher: IndexDataEnricher; lib: { isEsError: typeof isEsError; + parseEsError: typeof parseEsError; handleEsError: typeof handleEsError; }; } diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_fields/README.md index d4664a3a07c61..e682d77f7a884 100644 --- a/x-pack/plugins/runtime_fields/README.md +++ b/x-pack/plugins/runtime_fields/README.md @@ -35,7 +35,7 @@ const MyComponent = () => { const saveRuntimeField = (field: RuntimeField) => { // Do something with the field - console.log(field); // { name: 'myField', type: 'boolean', script: "return 'hello'" } + // See interface returned in @returns section below }; const openRuntimeFieldsEditor = async() => { @@ -45,6 +45,7 @@ const MyComponent = () => { closeRuntimeFieldEditor.current = openEditor({ onSave: saveRuntimeField, /* defaultValue: optional field to edit */ + /* ctx: Context -- see section below */ }); }; @@ -61,7 +62,40 @@ const MyComponent = () => { } ``` -#### Alternative +#### `@returns` + +You get back a `RuntimeField` object with the following interface + +```ts +interface RuntimeField { + name: string; + type: RuntimeType; // 'long' | 'boolean' ... + script: { + source: string; + } +} +``` + +#### Context object + +You can provide a context object to the runtime field editor. It has the following interface + +```ts +interface Context { + /** An array of field name not allowed. You would probably provide an array of existing runtime fields + * to prevent the user creating a field with the same name. + */ + namesNotAllowed?: string[]; + /** + * An array of existing concrete fields. If the user gives a name to the runtime + * field that matches one of the concrete fields, a callout will be displayed + * to indicate that this runtime field will shadow the concrete field. + */ + existingConcreteFields?: string[]; +} +``` + +#### Other type of integration The runtime field editor is also exported as static React component that you can import into your components. The editor is exported in 2 flavours: @@ -96,6 +130,7 @@ const MyComponent = () => { onCancel={() => setIsFlyoutVisible(false)} docLinks={docLinksStart} defaultValue={/*optional runtime field to edit*/} + ctx={/*optional context object -- see section above*/} /> </EuiFlyout> )} @@ -138,6 +173,7 @@ const MyComponent = () => { onCancel={() => flyoutEditor.current?.close()} docLinks={docLinksStart} defaultValue={defaultRuntimeField} + ctx={/*optional context object -- see section above*/} /> </KibanaReactContextProvider> ) @@ -182,6 +218,7 @@ const MyComponent = () => { onChange={setRuntimeFieldFormState} docLinks={docLinksStart} defaultValue={/*optional runtime field to edit*/} + ctx={/*optional context object -- see section above*/} /> <EuiSpacer /> diff --git a/x-pack/plugins/runtime_fields/public/components/index.ts b/x-pack/plugins/runtime_fields/public/components/index.ts index 86ac968d39f21..bccce5d591b51 100644 --- a/x-pack/plugins/runtime_fields/public/components/index.ts +++ b/x-pack/plugins/runtime_fields/public/components/index.ts @@ -8,4 +8,7 @@ export { RuntimeFieldForm, FormState as RuntimeFieldFormState } from './runtime_ export { RuntimeFieldEditor } from './runtime_field_editor'; -export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content'; +export { + RuntimeFieldEditorFlyoutContent, + RuntimeFieldEditorFlyoutContentProps, +} from './runtime_field_editor_flyout_content'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx index c56bc16c304ad..a8f90810a1212 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx @@ -31,6 +31,14 @@ describe('Runtime field editor', () => { const lastOnChangeCall = (): FormState[] => onChange.mock.calls[onChange.mock.calls.length - 1]; + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + beforeEach(() => { onChange = jest.fn(); }); @@ -46,7 +54,7 @@ describe('Runtime field editor', () => { const defaultValue: RuntimeField = { name: 'foo', type: 'date', - script: 'test=123', + script: { source: 'test=123' }, }; testBed = setup({ onChange, defaultValue, docLinks }); @@ -68,4 +76,75 @@ describe('Runtime field editor', () => { expect(lastState.isValid).toBe(true); expect(lastState.isSubmitted).toBe(true); }); + + test('should accept a list of existing concrete fields and display a callout when shadowing one of the fields', async () => { + const existingConcreteFields = ['myConcreteField']; + + testBed = setup({ onChange, docLinks, ctx: { existingConcreteFields } }); + + const { form, component, exists } = testBed; + + expect(exists('shadowingFieldCallout')).toBe(false); + + await act(async () => { + form.setInputValue('nameField.input', existingConcreteFields[0]); + }); + component.update(); + + expect(exists('shadowingFieldCallout')).toBe(true); + }); + + describe('validation', () => { + test('should accept an optional list of existing runtime fields and prevent creating duplicates', async () => { + const existingRuntimeFieldNames = ['myRuntimeField']; + + testBed = setup({ onChange, docLinks, ctx: { namesNotAllowed: existingRuntimeFieldNames } }); + + const { form, component } = testBed; + + await act(async () => { + form.setInputValue('nameField.input', existingRuntimeFieldNames[0]); + form.setInputValue('scriptField', 'echo("hello")'); + }); + + act(() => { + jest.advanceTimersByTime(1000); // Make sure our debounced error message is in the DOM + }); + + await act(async () => { + await lastOnChangeCall()[0].submit(); + }); + + component.update(); + + expect(lastOnChangeCall()[0].isValid).toBe(false); + expect(form.getErrorsMessages()).toEqual(['There is already a field with this name.']); + }); + + test('should not count the default value as a duplicate', async () => { + const existingRuntimeFieldNames = ['myRuntimeField']; + + const defaultValue: RuntimeField = { + name: 'myRuntimeField', + type: 'boolean', + script: { source: 'emit("hello"' }, + }; + + testBed = setup({ + defaultValue, + onChange, + docLinks, + ctx: { namesNotAllowed: existingRuntimeFieldNames }, + }); + + const { form } = testBed; + + await act(async () => { + await lastOnChangeCall()[0].submit(); + }); + + expect(lastOnChangeCall()[0].isValid).toBe(true); + expect(form.getErrorsMessages()).toEqual([]); + }); + }); }); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx index 07935be171fd2..2472ccbda062f 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx @@ -15,10 +15,13 @@ export interface Props { docLinks: DocLinksStart; defaultValue?: RuntimeField; onChange?: FormProps['onChange']; + ctx?: FormProps['ctx']; } -export const RuntimeFieldEditor = ({ defaultValue, onChange, docLinks }: Props) => { +export const RuntimeFieldEditor = ({ defaultValue, onChange, docLinks, ctx }: Props) => { const links = getLinks(docLinks); - return <RuntimeFieldForm links={links} defaultValue={defaultValue} onChange={onChange} />; + return ( + <RuntimeFieldForm links={links} defaultValue={defaultValue} onChange={onChange} ctx={ctx} /> + ); }; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts index 32234bfcc5600..ad6151b53546a 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content'; +export { + RuntimeFieldEditorFlyoutContent, + Props as RuntimeFieldEditorFlyoutContentProps, +} from './runtime_field_editor_flyout_content'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx index 8e47472295f45..972471d2e8190 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx @@ -39,7 +39,7 @@ describe('Runtime field editor flyout', () => { const field: RuntimeField = { name: 'foo', type: 'date', - script: 'test=123', + script: { source: 'test=123' }, }; const { find } = setup({ ...defaultProps, defaultValue: field }); @@ -47,14 +47,14 @@ describe('Runtime field editor flyout', () => { expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`); expect(find('nameField.input').props().value).toBe(field.name); expect(find('typeField').props().value).toBe(field.type); - expect(find('scriptField').props().value).toBe(field.script); + expect(find('scriptField').props().value).toBe(field.script.source); }); test('should accept an onSave prop', async () => { const field: RuntimeField = { name: 'foo', type: 'date', - script: 'test=123', + script: { source: 'test=123' }, }; const onSave: jest.Mock<Props['onSave']> = jest.fn(); @@ -93,10 +93,7 @@ describe('Runtime field editor flyout', () => { expect(onSave).toHaveBeenCalledTimes(0); expect(find('saveFieldButton').props().disabled).toBe(true); - expect(form.getErrorsMessages()).toEqual([ - 'Give a name to the field.', - 'Script must emit() a value.', - ]); + expect(form.getErrorsMessages()).toEqual(['Give a name to the field.']); expect(exists('formError')).toBe(true); expect(find('formError').text()).toBe('Fix errors in form before continuing.'); }); @@ -120,7 +117,7 @@ describe('Runtime field editor flyout', () => { expect(fieldReturned).toEqual({ name: 'someName', type: 'keyword', // default to keyword - script: 'script=123', + script: { source: 'script=123' }, }); // Change the type and make sure it is forwarded @@ -139,7 +136,7 @@ describe('Runtime field editor flyout', () => { expect(fieldReturned).toEqual({ name: 'someName', type: 'other_type', - script: 'script=123', + script: { source: 'script=123' }, }); }); }); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx index c7454cff0eb15..190cfb0deebcf 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx @@ -21,7 +21,10 @@ import { DocLinksStart } from 'src/core/public'; import { RuntimeField } from '../../types'; import { FormState } from '../runtime_field_form'; -import { RuntimeFieldEditor } from '../runtime_field_editor'; +import { + RuntimeFieldEditor, + Props as RuntimeFieldEditorProps, +} from '../runtime_field_editor/runtime_field_editor'; const geti18nTexts = (field?: RuntimeField) => { return { @@ -64,6 +67,10 @@ export interface Props { * An optional runtime field to edit */ defaultValue?: RuntimeField; + /** + * Optional context object + */ + ctx?: RuntimeFieldEditorProps['ctx']; } export const RuntimeFieldEditorFlyoutContent = ({ @@ -71,6 +78,7 @@ export const RuntimeFieldEditorFlyoutContent = ({ onCancel, docLinks, defaultValue: field, + ctx, }: Props) => { const i18nTexts = geti18nTexts(field); @@ -95,12 +103,17 @@ export const RuntimeFieldEditorFlyoutContent = ({ <> <EuiFlyoutHeader> <EuiTitle size="m" data-test-subj="flyoutTitle"> - <h2>{i18nTexts.flyoutTitle}</h2> + <h2 id="runtimeFieldEditorEditTitle">{i18nTexts.flyoutTitle}</h2> </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <RuntimeFieldEditor docLinks={docLinks} defaultValue={field} onChange={setFormState} /> + <RuntimeFieldEditor + docLinks={docLinks} + defaultValue={field} + onChange={setFormState} + ctx={ctx} + /> </EuiFlyoutBody> <EuiFlyoutFooter> diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx index 1829514856eed..9714ff43288dd 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx @@ -18,7 +18,7 @@ const setup = (props?: Props) => })(props) as TestBed; const links = { - painlessSyntax: 'https://jestTest.elastic.co/to-be-defined.html', + runtimePainless: 'https://jestTest.elastic.co/to-be-defined.html', }; describe('Runtime field form', () => { @@ -45,28 +45,28 @@ describe('Runtime field form', () => { const { exists, find } = testBed; expect(exists('painlessSyntaxLearnMoreLink')).toBe(true); - expect(find('painlessSyntaxLearnMoreLink').props().href).toBe(links.painlessSyntax); + expect(find('painlessSyntaxLearnMoreLink').props().href).toBe(links.runtimePainless); }); test('should accept a "defaultValue" prop', () => { const defaultValue: RuntimeField = { name: 'foo', type: 'date', - script: 'test=123', + script: { source: 'test=123' }, }; testBed = setup({ defaultValue, links }); const { find } = testBed; expect(find('nameField.input').props().value).toBe(defaultValue.name); expect(find('typeField').props().value).toBe(defaultValue.type); - expect(find('scriptField').props().value).toBe(defaultValue.script); + expect(find('scriptField').props().value).toBe(defaultValue.script.source); }); test('should accept an "onChange" prop to forward the form state', async () => { const defaultValue: RuntimeField = { name: 'foo', type: 'date', - script: 'test=123', + script: { source: 'test=123' }, }; testBed = setup({ onChange, defaultValue, links }); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx index 6068302f5b269..2ed6df537a6fe 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -14,9 +14,20 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiLink, + EuiCallOut, } from '@elastic/eui'; -import { useForm, Form, FormHook, UseField, TextField, CodeEditor } from '../../shared_imports'; +import { + useForm, + useFormData, + Form, + FormHook, + UseField, + TextField, + CodeEditor, + ValidationFunc, + FieldConfig, +} from '../../shared_imports'; import { RuntimeField } from '../../types'; import { RUNTIME_FIELD_OPTIONS } from '../../constants'; import { schema } from './schema'; @@ -29,15 +40,82 @@ export interface FormState { export interface Props { links: { - painlessSyntax: string; + runtimePainless: string; }; defaultValue?: RuntimeField; onChange?: (state: FormState) => void; + /** + * Optional context object + */ + ctx?: { + /** An array of field name not allowed */ + namesNotAllowed?: string[]; + /** + * An array of existing concrete fields. If the user gives a name to the runtime + * field that matches one of the concrete fields, a callout will be displayed + * to indicate that this runtime field will shadow the concrete field. + */ + existingConcreteFields?: string[]; + }; } -const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { +const createNameNotAllowedValidator = ( + namesNotAllowed: string[] +): ValidationFunc<{}, string, string> => ({ value }) => { + if (namesNotAllowed.includes(value)) { + return { + message: i18n.translate( + 'xpack.runtimeFields.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage', + { + defaultMessage: 'There is already a field with this name.', + } + ), + }; + } +}; + +/** + * Dynamically retrieve the config for the "name" field, adding + * a validator to avoid duplicated runtime fields to be created. + * + * @param namesNotAllowed Array of names not allowed for the field "name" + * @param defaultValue Initial value of the form + */ +const getNameFieldConfig = ( + namesNotAllowed?: string[], + defaultValue?: Props['defaultValue'] +): FieldConfig<string, RuntimeField> => { + const nameFieldConfig = schema.name as FieldConfig<string, RuntimeField>; + + if (!namesNotAllowed) { + return nameFieldConfig; + } + + // Add validation to not allow duplicates + return { + ...nameFieldConfig!, + validations: [ + ...(nameFieldConfig.validations ?? []), + { + validator: createNameNotAllowedValidator( + namesNotAllowed.filter((name) => name !== defaultValue?.name) + ), + }, + ], + }; +}; + +const RuntimeFieldFormComp = ({ + defaultValue, + onChange, + links, + ctx: { namesNotAllowed, existingConcreteFields = [] } = {}, +}: Props) => { const { form } = useForm<RuntimeField>({ defaultValue, schema }); const { submit, isValid: isFormValid, isSubmitted } = form; + const [{ name }] = useFormData<RuntimeField>({ form, watch: 'name' }); + + const nameFieldConfig = getNameFieldConfig(namesNotAllowed, defaultValue); useEffect(() => { if (onChange) { @@ -50,7 +128,19 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { <EuiFlexGroup> {/* Name */} <EuiFlexItem> - <UseField path="name" component={TextField} data-test-subj="nameField" /> + <UseField<string, RuntimeField> + path="name" + config={nameFieldConfig} + component={TextField} + data-test-subj="nameField" + componentProps={{ + euiFieldProps: { + 'aria-label': i18n.translate('xpack.runtimeFields.form.nameAriaLabel', { + defaultMessage: 'Name field', + }), + }, + }} + /> </EuiFlexItem> {/* Return type */} @@ -82,6 +172,9 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { }} isClearable={false} data-test-subj="typeField" + aria-label={i18n.translate('xpack.runtimeFields.form.typeSelectAriaLabel', { + defaultMessage: 'Type select', + })} fullWidth /> </EuiFormRow> @@ -92,10 +185,32 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { </EuiFlexItem> </EuiFlexGroup> + {existingConcreteFields.includes(name) && ( + <> + <EuiSpacer /> + <EuiCallOut + title={i18n.translate('xpack.runtimeFields.form.fieldShadowingCalloutTitle', { + defaultMessage: 'Field shadowing', + })} + color="warning" + iconType="pin" + size="s" + data-test-subj="shadowingFieldCallout" + > + <div> + {i18n.translate('xpack.runtimeFields.form.fieldShadowingCalloutDescription', { + defaultMessage: + 'This field shares the name of a mapped field. Values for this field will be returned in search results.', + })} + </div> + </EuiCallOut> + </> + )} + <EuiSpacer size="l" /> {/* Script */} - <UseField<string> path="script"> + <UseField<string> path="script.source"> {({ value, setValue, label, isValid, getErrorsMessages }) => { return ( <EuiFormRow @@ -106,7 +221,7 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { <EuiFlexGroup justifyContent="flexEnd"> <EuiFlexItem grow={false}> <EuiLink - href={links.painlessSyntax} + href={links.runtimePainless} target="_blank" external data-test-subj="painlessSyntaxLearnMoreLink" @@ -137,6 +252,9 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { automaticLayout: true, }} data-test-subj="scriptField" + aria-label={i18n.translate('xpack.runtimeFields.form.scriptEditorAriaLabel', { + defaultMessage: 'Script editor', + })} /> </EuiFormRow> ); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts index abb7cf812200f..9db23ef5291a0 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts @@ -42,17 +42,10 @@ export const schema: FormSchema<RuntimeField> = { serializer: (value: Array<ComboBoxOption<RuntimeType>>) => value[0].value!, }, script: { - label: i18n.translate('xpack.runtimeFields.form.defineFieldLabel', { - defaultMessage: 'Define field', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.runtimeFields.form.validations.scriptIsRequiredErrorMessage', { - defaultMessage: 'Script must emit() a value.', - }) - ), - }, - ], + source: { + label: i18n.translate('xpack.runtimeFields.form.defineFieldLabel', { + defaultMessage: 'Define field (optional)', + }), + }, }, }; diff --git a/x-pack/plugins/runtime_fields/public/index.ts b/x-pack/plugins/runtime_fields/public/index.ts index 0eab32c0b3d97..3f5b8002da132 100644 --- a/x-pack/plugins/runtime_fields/public/index.ts +++ b/x-pack/plugins/runtime_fields/public/index.ts @@ -7,6 +7,7 @@ import { RuntimeFieldsPlugin } from './plugin'; export { RuntimeFieldEditorFlyoutContent, + RuntimeFieldEditorFlyoutContentProps, RuntimeFieldEditor, RuntimeFieldFormState, } from './components'; diff --git a/x-pack/plugins/runtime_fields/public/lib/documentation.ts b/x-pack/plugins/runtime_fields/public/lib/documentation.ts index 87eab8b7ed997..4f7eb10aa7c77 100644 --- a/x-pack/plugins/runtime_fields/public/lib/documentation.ts +++ b/x-pack/plugins/runtime_fields/public/lib/documentation.ts @@ -8,9 +8,11 @@ import { DocLinksStart } from 'src/core/public'; export const getLinks = (docLinks: DocLinksStart) => { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; return { + runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, }; }; diff --git a/x-pack/plugins/runtime_fields/public/load_editor.tsx b/x-pack/plugins/runtime_fields/public/load_editor.tsx index da2819411732b..bf13e79caad0f 100644 --- a/x-pack/plugins/runtime_fields/public/load_editor.tsx +++ b/x-pack/plugins/runtime_fields/public/load_editor.tsx @@ -8,10 +8,12 @@ import { CoreSetup, OverlayRef } from 'src/core/public'; import { toMountPoint, createKibanaReactContext } from './shared_imports'; import { LoadEditorResponse, RuntimeField } from './types'; +import { RuntimeFieldEditorFlyoutContentProps } from './components'; export interface OpenRuntimeFieldEditorProps { onSave(field: RuntimeField): void; - defaultValue?: RuntimeField; + defaultValue?: RuntimeFieldEditorFlyoutContentProps['defaultValue']; + ctx?: RuntimeFieldEditorFlyoutContentProps['ctx']; } export const getRuntimeFieldEditorLoader = ( @@ -24,10 +26,12 @@ export const getRuntimeFieldEditorLoader = ( let overlayRef: OverlayRef | null = null; - const openEditor = ({ onSave, defaultValue }: OpenRuntimeFieldEditorProps) => { + const openEditor = ({ onSave, defaultValue, ctx }: OpenRuntimeFieldEditorProps) => { const closeEditor = () => { - overlayRef?.close(); - overlayRef = null; + if (overlayRef) { + overlayRef.close(); + overlayRef = null; + } }; const onSaveField = (field: RuntimeField) => { @@ -43,6 +47,7 @@ export const getRuntimeFieldEditorLoader = ( onCancel={() => overlayRef?.close()} docLinks={docLinks} defaultValue={defaultValue} + ctx={ctx} /> </KibanaReactContextProvider> ) diff --git a/x-pack/plugins/runtime_fields/public/shared_imports.ts b/x-pack/plugins/runtime_fields/public/shared_imports.ts index 200a68ab71031..44ada67dc0014 100644 --- a/x-pack/plugins/runtime_fields/public/shared_imports.ts +++ b/x-pack/plugins/runtime_fields/public/shared_imports.ts @@ -6,10 +6,13 @@ export { useForm, + useFormData, Form, FormSchema, UseField, FormHook, + ValidationFunc, + FieldConfig, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; diff --git a/x-pack/plugins/runtime_fields/public/types.ts b/x-pack/plugins/runtime_fields/public/types.ts index 4172061540af8..b1bbb06d79655 100644 --- a/x-pack/plugins/runtime_fields/public/types.ts +++ b/x-pack/plugins/runtime_fields/public/types.ts @@ -31,7 +31,9 @@ export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { name: string; type: RuntimeType; - script: string; + script: { + source: string; + }; } export interface ComboBoxOption<T = unknown> { diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index 8d491e6a135ea..dd5dac5626041 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -191,6 +191,26 @@ export default function ({ getService }) { '[request body.indexPatterns]: expected value of type [array] ' ); }); + + it('should parse the ES error and return the cause', async () => { + const templateName = `template-${getRandomString()}`; + const payload = getTemplatePayload(templateName, [getRandomString()]); + const runtime = { + myRuntimeField: { + type: 'boolean', + script: { + source: 'emit("hello with error', // error in script + }, + }, + }; + payload.template.mappings = { ...payload.template.mappings, runtime }; + const { body } = await createTemplate(payload).expect(400); + + expect(body.attributes).an('object'); + expect(body.attributes.message).contain('template after composition is invalid'); + // one of the item of the cause array should point to our script + expect(body.attributes.cause.join(',')).contain('"hello with error'); + }); }); describe('update', () => { @@ -248,6 +268,32 @@ export default function ({ getService }) { catTemplateResponse.find(({ name: templateName }) => templateName === name).version ).to.equal(updatedVersion.toString()); }); + + it('should parse the ES error and return the cause', async () => { + const templateName = `template-${getRandomString()}`; + const payload = getTemplatePayload(templateName, [getRandomString()]); + const runtime = { + myRuntimeField: { + type: 'keyword', + script: { + source: 'emit("hello")', + }, + }, + }; + + // Add runtime field + payload.template.mappings = { ...payload.template.mappings, runtime }; + + await createTemplate(payload).expect(200); + + // Update template with an error in the runtime field script + payload.template.mappings.runtime.myRuntimeField.script = 'emit("hello with error'; + const { body } = await updateTemplate(payload, templateName).expect(400); + + expect(body.attributes).an('object'); + // one of the item of the cause array should point to our script + expect(body.attributes.cause.join(',')).contain('"hello with error'); + }); }); describe('delete', () => { From 7393c230a43b00fdb53d01aa0b2820c010624110 Mon Sep 17 00:00:00 2001 From: Luke Elmers <luke.elmers@elastic.co> Date: Thu, 3 Dec 2020 08:09:23 -0700 Subject: [PATCH 101/107] [data.search.searchSource] Update SearchSource to use Fields API. (#82383) --- ...gins-data-public.searchsource.getfields.md | 6 +- ...s-data-public.searchsourcefields.fields.md | 4 +- ...lic.searchsourcefields.fieldsfromsource.md | 18 + ...-plugins-data-public.searchsourcefields.md | 3 +- .../search_examples/public/components/app.tsx | 380 ++++++++++----- .../filter_docvalue_fields.test.ts | 30 -- .../search_source/filter_docvalue_fields.ts | 33 -- .../search_source/search_source.test.ts | 442 +++++++++++++++--- .../search/search_source/search_source.ts | 158 +++++-- .../data/common/search/search_source/types.ts | 18 +- src/plugins/data/public/public.api.md | 9 +- .../helpers/get_sharing_data.test.ts | 8 +- .../application/helpers/get_sharing_data.ts | 2 +- .../__snapshots__/add_filter.test.tsx.snap | 4 +- .../components/add_filter/add_filter.tsx | 2 +- .../confirmation_modal.test.tsx.snap | 2 +- .../confirmation_modal/confirmation_modal.tsx | 2 +- .../header/__snapshots__/header.test.tsx.snap | 4 +- .../components/header/header.tsx | 9 +- .../edit_index_pattern/tabs/utils.ts | 2 +- .../es_search_source/es_search_source.test.ts | 2 +- .../es_search_source/es_search_source.tsx | 6 +- .../functional/apps/maps/mvt_super_fine.js | 2 +- 23 files changed, 834 insertions(+), 312 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md delete mode 100644 src/plugins/data/common/search/search_source/filter_docvalue_fields.test.ts delete mode 100644 src/plugins/data/common/search/search_source/filter_docvalue_fields.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md index 1980227bee623..13b97ab4121c8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md @@ -21,7 +21,8 @@ getFields(): { size?: number | undefined; source?: string | boolean | string[] | undefined; version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; + fields?: SearchFieldValue[] | undefined; + fieldsFromSource?: string | boolean | string[] | undefined; index?: import("../..").IndexPattern | undefined; searchAfter?: import("./types").EsQuerySearchAfter | undefined; timeout?: string | undefined; @@ -42,7 +43,8 @@ getFields(): { size?: number | undefined; source?: string | boolean | string[] | undefined; version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; + fields?: SearchFieldValue[] | undefined; + fieldsFromSource?: string | boolean | string[] | undefined; index?: import("../..").IndexPattern | undefined; searchAfter?: import("./types").EsQuerySearchAfter | undefined; timeout?: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fields.md index 21d09910bd2b9..87f6a0cb7b80f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fields.md @@ -4,8 +4,10 @@ ## SearchSourceFields.fields property +Retrieve fields via the search Fields API + <b>Signature:</b> ```typescript -fields?: NameList; +fields?: SearchFieldValue[]; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md new file mode 100644 index 0000000000000..d343d8ce180da --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md @@ -0,0 +1,18 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) > [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md) + +## SearchSourceFields.fieldsFromSource property + +> Warning: This API is now obsolete. +> +> It is recommended to use `fields` wherever possible. +> + +Retreive fields directly from \_source (legacy behavior) + +<b>Signature:</b> + +```typescript +fieldsFromSource?: NameList; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md index d19f1da439cee..683a35fabf571 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md @@ -17,7 +17,8 @@ export interface SearchSourceFields | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | <code>any</code> | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | -| [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | <code>NameList</code> | | +| [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | <code>SearchFieldValue[]</code> | Retrieve fields via the search Fields API | +| [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md) | <code>NameList</code> | Retreive fields directly from \_source (legacy behavior) | | [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | <code>Filter[] | Filter | (() => Filter[] | Filter | undefined)</code> | [Filter](./kibana-plugin-plugins-data-public.filter.md) | | [from](./kibana-plugin-plugins-data-public.searchsourcefields.from.md) | <code>number</code> | | | [highlight](./kibana-plugin-plugins-data-public.searchsourcefields.highlight.md) | <code>any</code> | | diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 2425f3bbad8a9..33ad8bbfe3d35 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -23,7 +23,8 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { - EuiButton, + EuiButtonEmpty, + EuiCodeBlock, EuiPage, EuiPageBody, EuiPageContent, @@ -32,6 +33,7 @@ import { EuiTitle, EuiText, EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, EuiCheckbox, EuiSpacer, @@ -68,6 +70,11 @@ interface SearchExamplesAppDeps { data: DataPublicPluginStart; } +function getNumeric(fields?: IndexPatternField[]) { + if (!fields) return []; + return fields?.filter((f) => f.type === 'number' && f.aggregatable); +} + function formatFieldToComboBox(field?: IndexPatternField | null) { if (!field) return []; return formatFieldsToComboBox([field]); @@ -95,8 +102,13 @@ export const SearchExamplesApp = ({ const [getCool, setGetCool] = useState<boolean>(false); const [timeTook, setTimeTook] = useState<number | undefined>(); const [indexPattern, setIndexPattern] = useState<IndexPattern | null>(); - const [numericFields, setNumericFields] = useState<IndexPatternField[]>(); - const [selectedField, setSelectedField] = useState<IndexPatternField | null | undefined>(); + const [fields, setFields] = useState<IndexPatternField[]>(); + const [selectedFields, setSelectedFields] = useState<IndexPatternField[]>([]); + const [selectedNumericField, setSelectedNumericField] = useState< + IndexPatternField | null | undefined + >(); + const [request, setRequest] = useState<Record<string, any>>({}); + const [response, setResponse] = useState<Record<string, any>>({}); // Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted. useEffect(() => { @@ -110,24 +122,23 @@ export const SearchExamplesApp = ({ // Update the fields list every time the index pattern is modified. useEffect(() => { - const fields = indexPattern?.fields.filter( - (field) => field.type === 'number' && field.aggregatable - ); - setNumericFields(fields); - setSelectedField(fields?.length ? fields[0] : null); + setFields(indexPattern?.fields); }, [indexPattern]); + useEffect(() => { + setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null); + }, [fields]); const doAsyncSearch = async (strategy?: string) => { - if (!indexPattern || !selectedField) return; + if (!indexPattern || !selectedNumericField) return; // Constuct the query portion of the search request const query = data.query.getEsQuery(indexPattern); // Constuct the aggregations portion of the search request by using the `data.search.aggs` service. - const aggs = [{ type: 'avg', params: { field: selectedField.name } }]; + const aggs = [{ type: 'avg', params: { field: selectedNumericField!.name } }]; const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl(); - const request = { + const req = { params: { index: indexPattern.title, body: { @@ -140,23 +151,26 @@ export const SearchExamplesApp = ({ }; // Submit the search request using the `data.search` service. + setRequest(req.params.body); const searchSubscription$ = data.search - .search(request, { + .search(req, { strategy, }) .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - setTimeTook(response.rawResponse.took); - const avgResult: number | undefined = response.rawResponse.aggregations - ? response.rawResponse.aggregations[1].value + next: (res) => { + if (isCompleteResponse(res)) { + setResponse(res.rawResponse); + setTimeTook(res.rawResponse.took); + const avgResult: number | undefined = res.rawResponse.aggregations + ? res.rawResponse.aggregations[1].value : undefined; const message = ( <EuiText> - Searched {response.rawResponse.hits.total} documents. <br /> - The average of {selectedField.name} is {avgResult ? Math.floor(avgResult) : 0}. + Searched {res.rawResponse.hits.total} documents. <br /> + The average of {selectedNumericField!.name} is{' '} + {avgResult ? Math.floor(avgResult) : 0}. <br /> - Is it Cool? {String((response as IMyStrategyResponse).cool)} + Is it Cool? {String((res as IMyStrategyResponse).cool)} </EuiText> ); notifications.toasts.addSuccess({ @@ -164,7 +178,7 @@ export const SearchExamplesApp = ({ text: mountReactNode(message), }); searchSubscription$.unsubscribe(); - } else if (isErrorResponse(response)) { + } else if (isErrorResponse(res)) { // TODO: Make response error status clearer notifications.toasts.addWarning('An error has occurred'); searchSubscription$.unsubscribe(); @@ -176,6 +190,50 @@ export const SearchExamplesApp = ({ }); }; + const doSearchSourceSearch = async () => { + if (!indexPattern) return; + + const query = data.query.queryString.getQuery(); + const filters = data.query.filterManager.getFilters(); + const timefilter = data.query.timefilter.timefilter.createFilter(indexPattern); + if (timefilter) { + filters.push(timefilter); + } + + try { + const searchSource = await data.search.searchSource.create(); + + searchSource + .setField('index', indexPattern) + .setField('filter', filters) + .setField('query', query) + .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']); + + if (selectedNumericField) { + searchSource.setField('aggs', () => { + return data.search.aggs + .createAggConfigs(indexPattern, [ + { type: 'avg', params: { field: selectedNumericField.name } }, + ]) + .toDsl(); + }); + } + + setRequest(await searchSource.getSearchRequestBody()); + const res = await searchSource.fetch(); + setResponse(res); + + const message = <EuiText>Searched {res.hits.total} documents.</EuiText>; + notifications.toasts.addSuccess({ + title: 'Query result', + text: mountReactNode(message), + }); + } catch (e) { + setResponse(e.body); + notifications.toasts.addWarning(`An error has occurred: ${e.message}`); + } + }; + const onClickHandler = () => { doAsyncSearch(); }; @@ -185,22 +243,24 @@ export const SearchExamplesApp = ({ }; const onServerClickHandler = async () => { - if (!indexPattern || !selectedField) return; + if (!indexPattern || !selectedNumericField) return; try { - const response = await http.get(SERVER_SEARCH_ROUTE_PATH, { + const res = await http.get(SERVER_SEARCH_ROUTE_PATH, { query: { index: indexPattern.title, - field: selectedField.name, + field: selectedNumericField!.name, }, }); - notifications.toasts.addSuccess(`Server returned ${JSON.stringify(response)}`); + notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`); } catch (e) { notifications.toasts.addDanger('Failed to run search'); } }; - if (!indexPattern) return null; + const onSearchSourceClickHandler = () => { + doSearchSourceSearch(); + }; return ( <Router basename={basename}> @@ -212,7 +272,7 @@ export const SearchExamplesApp = ({ useDefaultBehaviors={true} indexPatterns={indexPattern ? [indexPattern] : undefined} /> - <EuiPage restrictWidth="1000px"> + <EuiPage> <EuiPageBody> <EuiPageHeader> <EuiTitle size="l"> @@ -227,106 +287,178 @@ export const SearchExamplesApp = ({ </EuiPageHeader> <EuiPageContent> <EuiPageContentBody> - <EuiText> - <EuiFlexGrid columns={1}> - <EuiFlexItem> - <EuiFormLabel>Index Pattern</EuiFormLabel> - <IndexPatternSelect - placeholder={i18n.translate( - 'backgroundSessionExample.selectIndexPatternPlaceholder', - { - defaultMessage: 'Select index pattern', - } - )} - indexPatternId={indexPattern?.id || ''} - onChange={async (newIndexPatternId: any) => { - const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); - setIndexPattern(newIndexPattern); - }} - isClearable={false} - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormLabel>Numeric Fields</EuiFormLabel> - <EuiComboBox - options={formatFieldsToComboBox(numericFields)} - selectedOptions={formatFieldToComboBox(selectedField)} - singleSelection={true} - onChange={(option) => { - const field = indexPattern.getFieldByName(option[0].label); - setSelectedField(field || null); - }} - sortMatchesBy="startsWith" + <EuiFlexGrid columns={3}> + <EuiFlexItem style={{ width: '40%' }}> + <EuiText> + <EuiFlexGrid columns={2}> + <EuiFlexItem> + <EuiFormLabel>Index Pattern</EuiFormLabel> + <IndexPatternSelect + placeholder={i18n.translate( + 'backgroundSessionExample.selectIndexPatternPlaceholder', + { + defaultMessage: 'Select index pattern', + } + )} + indexPatternId={indexPattern?.id || ''} + onChange={async (newIndexPatternId: any) => { + const newIndexPattern = await data.indexPatterns.get( + newIndexPatternId + ); + setIndexPattern(newIndexPattern); + }} + isClearable={false} + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormLabel>Numeric Field to Aggregate</EuiFormLabel> + <EuiComboBox + options={formatFieldsToComboBox(getNumeric(fields))} + selectedOptions={formatFieldToComboBox(selectedNumericField)} + singleSelection={true} + onChange={(option) => { + const fld = indexPattern?.getFieldByName(option[0].label); + setSelectedNumericField(fld || null); + }} + sortMatchesBy="startsWith" + /> + </EuiFlexItem> + </EuiFlexGrid> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormLabel> + Fields to query (leave blank to include all fields) + </EuiFormLabel> + <EuiComboBox + options={formatFieldsToComboBox(fields)} + selectedOptions={formatFieldsToComboBox(selectedFields)} + singleSelection={false} + onChange={(option) => { + const flds = option + .map((opt) => indexPattern?.getFieldByName(opt?.label)) + .filter((f) => f); + setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []); + }} + sortMatchesBy="startsWith" + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiText> + <EuiSpacer /> + <EuiTitle size="s"> + <h3> + Searching Elasticsearch using <EuiCode>data.search</EuiCode> + </h3> + </EuiTitle> + <EuiText> + If you want to fetch data from Elasticsearch, you can use the different + services provided by the <EuiCode>data</EuiCode> plugin. These help you get + the index pattern and search bar configuration, format them into a DSL query + and send it to Elasticsearch. + <EuiSpacer /> + <EuiButtonEmpty size="xs" onClick={onClickHandler} iconType="play"> + <FormattedMessage + id="searchExamples.buttonText" + defaultMessage="Request from low-level client (data.search.search)" + /> + </EuiButtonEmpty> + <EuiButtonEmpty + size="xs" + onClick={onSearchSourceClickHandler} + iconType="play" + > + <FormattedMessage + id="searchExamples.searchSource.buttonText" + defaultMessage="Request from high-level client (data.search.searchSource)" + /> + </EuiButtonEmpty> + </EuiText> + <EuiSpacer /> + <EuiTitle size="s"> + <h3>Writing a custom search strategy</h3> + </EuiTitle> + <EuiText> + If you want to do some pre or post processing on the server, you might want + to create a custom search strategy. This example uses such a strategy, + passing in custom input and receiving custom output back. + <EuiSpacer /> + <EuiCheckbox + id="GetCool" + label={ + <FormattedMessage + id="searchExamples.getCoolCheckbox" + defaultMessage="Get cool parameter?" + /> + } + checked={getCool} + onChange={(event) => setGetCool(event.target.checked)} /> - </EuiFlexItem> - </EuiFlexGrid> - </EuiText> - <EuiText> - <FormattedMessage - id="searchExamples.timestampText" - defaultMessage="Last query took: {time} ms" - values={{ time: timeTook || 'Unknown' }} - /> - </EuiText> - <EuiSpacer /> - <EuiTitle size="s"> - <h3> - Searching Elasticsearch using <EuiCode>data.search</EuiCode> - </h3> - </EuiTitle> - <EuiText> - If you want to fetch data from Elasticsearch, you can use the different services - provided by the <EuiCode>data</EuiCode> plugin. These help you get the index - pattern and search bar configuration, format them into a DSL query and send it - to Elasticsearch. - <EuiSpacer /> - <EuiButton type="primary" size="s" onClick={onClickHandler}> - <FormattedMessage id="searchExamples.buttonText" defaultMessage="Get data" /> - </EuiButton> - </EuiText> - <EuiSpacer /> - <EuiTitle size="s"> - <h3>Writing a custom search strategy</h3> - </EuiTitle> - <EuiText> - If you want to do some pre or post processing on the server, you might want to - create a custom search strategy. This example uses such a strategy, passing in - custom input and receiving custom output back. - <EuiSpacer /> - <EuiCheckbox - id="GetCool" - label={ + <EuiButtonEmpty + size="xs" + onClick={onMyStrategyClickHandler} + iconType="play" + > + <FormattedMessage + id="searchExamples.myStrategyButtonText" + defaultMessage="Request from low-level client via My Strategy" + /> + </EuiButtonEmpty> + </EuiText> + <EuiSpacer /> + <EuiTitle size="s"> + <h3>Using search on the server</h3> + </EuiTitle> + <EuiText> + You can also run your search request from the server, without registering a + search strategy. This request does not take the configuration of{' '} + <EuiCode>TopNavMenu</EuiCode> into account, but you could pass those down to + the server as well. + <EuiSpacer /> + <EuiButtonEmpty size="xs" onClick={onServerClickHandler} iconType="play"> + <FormattedMessage + id="searchExamples.myServerButtonText" + defaultMessage="Request from low-level client on the server" + /> + </EuiButtonEmpty> + </EuiText> + </EuiFlexItem> + <EuiFlexItem style={{ width: '30%' }}> + <EuiTitle size="xs"> + <h4>Request</h4> + </EuiTitle> + <EuiText size="xs">Search body sent to ES</EuiText> + <EuiCodeBlock + language="json" + fontSize="s" + paddingSize="s" + overflowHeight={450} + isCopyable + > + {JSON.stringify(request, null, 2)} + </EuiCodeBlock> + </EuiFlexItem> + <EuiFlexItem style={{ width: '30%' }}> + <EuiTitle size="xs"> + <h4>Response</h4> + </EuiTitle> + <EuiText size="xs"> <FormattedMessage - id="searchExamples.getCoolCheckbox" - defaultMessage="Get cool parameter?" + id="searchExamples.timestampText" + defaultMessage="Took: {time} ms" + values={{ time: timeTook || 'Unknown' }} /> - } - checked={getCool} - onChange={(event) => setGetCool(event.target.checked)} - /> - <EuiButton type="primary" size="s" onClick={onMyStrategyClickHandler}> - <FormattedMessage - id="searchExamples.myStrategyButtonText" - defaultMessage="Get data via My Strategy" - /> - </EuiButton> - </EuiText> - <EuiSpacer /> - <EuiTitle size="s"> - <h3>Using search on the server</h3> - </EuiTitle> - <EuiText> - You can also run your search request from the server, without registering a - search strategy. This request does not take the configuration of{' '} - <EuiCode>TopNavMenu</EuiCode> into account, but you could pass those down to the - server as well. - <EuiButton type="primary" size="s" onClick={onServerClickHandler}> - <FormattedMessage - id="searchExamples.myServerButtonText" - defaultMessage="Get data on the server" - /> - </EuiButton> - </EuiText> + </EuiText> + <EuiCodeBlock + language="json" + fontSize="s" + paddingSize="s" + overflowHeight={450} + isCopyable + > + {JSON.stringify(response, null, 2)} + </EuiCodeBlock> + </EuiFlexItem> + </EuiFlexGrid> </EuiPageContentBody> </EuiPageContent> </EuiPageBody> diff --git a/src/plugins/data/common/search/search_source/filter_docvalue_fields.test.ts b/src/plugins/data/common/search/search_source/filter_docvalue_fields.test.ts deleted file mode 100644 index 522117fe22804..0000000000000 --- a/src/plugins/data/common/search/search_source/filter_docvalue_fields.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { filterDocvalueFields } from './filter_docvalue_fields'; - -test('Should exclude docvalue_fields that are not contained in fields', () => { - const docvalueFields = [ - 'my_ip_field', - { field: 'my_keyword_field' }, - { field: 'my_date_field', format: 'epoch_millis' }, - ]; - const out = filterDocvalueFields(docvalueFields, ['my_ip_field', 'my_keyword_field']); - expect(out).toEqual(['my_ip_field', { field: 'my_keyword_field' }]); -}); diff --git a/src/plugins/data/common/search/search_source/filter_docvalue_fields.ts b/src/plugins/data/common/search/search_source/filter_docvalue_fields.ts deleted file mode 100644 index bbac30d7dfdc5..0000000000000 --- a/src/plugins/data/common/search/search_source/filter_docvalue_fields.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -interface DocvalueField { - field: string; - [key: string]: unknown; -} - -export function filterDocvalueFields( - docvalueFields: Array<string | DocvalueField>, - fields: string[] -) { - return docvalueFields.filter((docValue) => { - const docvalueFieldName = typeof docValue === 'string' ? docValue : docValue.field; - return fields.includes(docvalueFieldName); - }); -} diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index e7bdcb159f3cb..d0c6f0456a8f1 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -29,7 +29,7 @@ jest.mock('./legacy', () => ({ const getComputedFields = () => ({ storedFields: [], - scriptFields: [], + scriptFields: {}, docvalueFields: [], }); @@ -51,6 +51,7 @@ const indexPattern2 = ({ describe('SearchSource', () => { let mockSearchMethod: any; let searchSourceDependencies: SearchSourceDependencies; + let searchSource: SearchSource; beforeEach(() => { mockSearchMethod = jest.fn().mockReturnValue(of({ rawResponse: '' })); @@ -64,19 +65,12 @@ describe('SearchSource', () => { loadingCount$: new BehaviorSubject(0), }, }; - }); - describe('#setField()', () => { - test('sets the value for the property', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); + searchSource = new SearchSource({}, searchSourceDependencies); }); describe('#getField()', () => { test('gets the value for the property', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('aggs', 5); expect(searchSource.getField('aggs')).toBe(5); }); @@ -84,52 +78,391 @@ describe('SearchSource', () => { describe('#removeField()', () => { test('remove property', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); + searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('aggs', 5); searchSource.removeField('aggs'); expect(searchSource.getField('aggs')).toBeFalsy(); }); }); - describe(`#setField('index')`, () => { - describe('auto-sourceFiltering', () => { - describe('new index pattern assigned', () => { - test('generates a searchSource filter', async () => { - const searchSource = new SearchSource({}, searchSourceDependencies); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - const request = await searchSource.getSearchRequestBody(); - expect(request._source).toBe(mockSource); + describe('#setField() / #flatten', () => { + test('sets the value for the property', () => { + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + + describe('computed fields handling', () => { + test('still provides computed fields when no fields are specified', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: ['hello'], + scriptFields: { world: {} }, + docvalueFields: ['@timestamp'], + }), + } as unknown) as IndexPattern); + + const request = await searchSource.getSearchRequestBody(); + expect(request.stored_fields).toEqual(['hello']); + expect(request.script_fields).toEqual({ world: {} }); + expect(request.fields).toEqual(['@timestamp']); + }); + + test('never includes docvalue_fields', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: ['@timestamp'], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['@timestamp']); + searchSource.setField('fieldsFromSource', ['foo']); + + const request = await searchSource.getSearchRequestBody(); + expect(request).not.toHaveProperty('docvalue_fields'); + }); + + test('overrides computed docvalue fields with ones that are provided', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: ['hello'], + }), + } as unknown) as IndexPattern); + // @ts-expect-error TS won't like using this field name, but technically it's possible. + searchSource.setField('docvalue_fields', ['world']); + + const request = await searchSource.getSearchRequestBody(); + expect(request).toHaveProperty('docvalue_fields'); + expect(request.docvalue_fields).toEqual(['world']); + }); + + test('allows explicitly provided docvalue fields to override fields API when fetching fieldsFromSource', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: [{ field: 'a', format: 'date_time' }], + }), + } as unknown) as IndexPattern); + // @ts-expect-error TS won't like using this field name, but technically it's possible. + searchSource.setField('docvalue_fields', [{ field: 'b', format: 'date_time' }]); + searchSource.setField('fields', ['c']); + searchSource.setField('fieldsFromSource', ['a', 'b', 'd']); + + const request = await searchSource.getSearchRequestBody(); + expect(request).toHaveProperty('docvalue_fields'); + expect(request._source.includes).toEqual(['c', 'a', 'b', 'd']); + expect(request.docvalue_fields).toEqual([{ field: 'b', format: 'date_time' }]); + expect(request.fields).toEqual(['c', { field: 'a', format: 'date_time' }]); + }); + + test('allows you to override computed fields if you provide a format', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: [{ field: 'hello', format: 'date_time' }], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', [{ field: 'hello', format: 'strict_date_time' }]); + + const request = await searchSource.getSearchRequestBody(); + expect(request).toHaveProperty('fields'); + expect(request.fields).toEqual([{ field: 'hello', format: 'strict_date_time' }]); + }); + + test('injects a date format for computed docvalue fields if none is provided', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: [{ field: 'hello', format: 'date_time' }], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['hello']); + + const request = await searchSource.getSearchRequestBody(); + expect(request).toHaveProperty('fields'); + expect(request.fields).toEqual([{ field: 'hello', format: 'date_time' }]); + }); + + test('injects a date format for computed docvalue fields while merging other properties', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: [{ field: 'hello', format: 'date_time', a: 'test', b: 'test' }], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', [{ field: 'hello', a: 'a', c: 'c' }]); + + const request = await searchSource.getSearchRequestBody(); + expect(request).toHaveProperty('fields'); + expect(request.fields).toEqual([ + { field: 'hello', format: 'date_time', a: 'a', b: 'test', c: 'c' }, + ]); + }); + + test('merges provided script fields with computed fields', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {} }, + docvalueFields: [], + }), + } as unknown) as IndexPattern); + // @ts-expect-error TS won't like using this field name, but technically it's possible. + searchSource.setField('script_fields', { world: {} }); + + const request = await searchSource.getSearchRequestBody(); + expect(request).toHaveProperty('script_fields'); + expect(request.script_fields).toEqual({ + hello: {}, + world: {}, }); + }); - test('removes created searchSource filter on removal', async () => { - const searchSource = new SearchSource({}, searchSourceDependencies); - searchSource.setField('index', indexPattern); - searchSource.setField('index', undefined); - const request = await searchSource.getSearchRequestBody(); - expect(request._source).toBe(undefined); + test(`requests any fields that aren't script_fields from stored_fields`, async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {} }, + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['hello', 'a', { field: 'c' }]); + + const request = await searchSource.getSearchRequestBody(); + expect(request.script_fields).toEqual({ hello: {} }); + expect(request.stored_fields).toEqual(['a', 'c']); + }); + + test('ignores objects without a `field` property when setting stored_fields', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {} }, + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['hello', 'a', { foo: 'c' }]); + + const request = await searchSource.getSearchRequestBody(); + expect(request.script_fields).toEqual({ hello: {} }); + expect(request.stored_fields).toEqual(['a']); + }); + + test(`requests any fields that aren't script_fields from stored_fields with fieldsFromSource`, async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {} }, + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fieldsFromSource', ['hello', 'a']); + + const request = await searchSource.getSearchRequestBody(); + expect(request.script_fields).toEqual({ hello: {} }); + expect(request.stored_fields).toEqual(['a']); + }); + + test('defaults to * for stored fields when no fields are provided', async () => { + const requestA = await searchSource.getSearchRequestBody(); + expect(requestA.stored_fields).toEqual(['*']); + + searchSource.setField('fields', ['*']); + const requestB = await searchSource.getSearchRequestBody(); + expect(requestB.stored_fields).toEqual(['*']); + }); + + test('defaults to * for stored fields when no fields are provided with fieldsFromSource', async () => { + searchSource.setField('fieldsFromSource', ['*']); + const request = await searchSource.getSearchRequestBody(); + expect(request.stored_fields).toEqual(['*']); + }); + }); + + describe('source filters handling', () => { + test('excludes docvalue fields based on source filtering', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: ['@timestamp', 'exclude-me'], + }), + } as unknown) as IndexPattern); + // @ts-expect-error Typings for excludes filters need to be fixed. + searchSource.setField('source', { excludes: ['exclude-*'] }); + + const request = await searchSource.getSearchRequestBody(); + expect(request.fields).toEqual(['@timestamp']); + }); + + test('defaults to source filters from index pattern', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: ['@timestamp', 'foo-bar', 'foo-baz'], + }), + } as unknown) as IndexPattern); + + const request = await searchSource.getSearchRequestBody(); + expect(request.fields).toEqual(['@timestamp']); + }); + + test('filters script fields to only include specified fields', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {}, world: {} }, + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['hello']); + + const request = await searchSource.getSearchRequestBody(); + expect(request.script_fields).toEqual({ hello: {} }); + }); + }); + + describe('handling for when specific fields are provided', () => { + test('fieldsFromSource will request any fields outside of script_fields from _source & stored fields', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {}, world: {} }, + docvalueFields: ['@timestamp'], + }), + } as unknown) as IndexPattern); + searchSource.setField('fieldsFromSource', [ + 'hello', + 'world', + '@timestamp', + 'foo-a', + 'bar-b', + ]); + + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toEqual({ + includes: ['@timestamp', 'bar-b'], }); + expect(request.stored_fields).toEqual(['@timestamp', 'bar-b']); + }); + + test('filters request when a specific list of fields is provided', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: ['*'], + scriptFields: { hello: {}, world: {} }, + docvalueFields: ['@timestamp', 'date'], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['hello', '@timestamp', 'foo-a', 'bar']); + + const request = await searchSource.getSearchRequestBody(); + expect(request.fields).toEqual(['hello', '@timestamp', 'bar']); + expect(request.script_fields).toEqual({ hello: {} }); + expect(request.stored_fields).toEqual(['@timestamp', 'bar']); }); - describe('new index pattern assigned over another', () => { - test('replaces searchSource filter with new', async () => { - const searchSource = new SearchSource({}, searchSourceDependencies); - searchSource.setField('index', indexPattern); - searchSource.setField('index', indexPattern2); - expect(searchSource.getField('index')).toBe(indexPattern2); - const request = await searchSource.getSearchRequestBody(); - expect(request._source).toBe(mockSource2); + test('filters request when a specific list of fields is provided with fieldsFromSource', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: ['*'], + scriptFields: { hello: {}, world: {} }, + docvalueFields: ['@timestamp', 'date'], + }), + } as unknown) as IndexPattern); + searchSource.setField('fieldsFromSource', ['hello', '@timestamp', 'foo-a', 'bar']); + + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toEqual({ + includes: ['@timestamp', 'bar'], }); + expect(request.fields).toEqual(['@timestamp']); + expect(request.script_fields).toEqual({ hello: {} }); + expect(request.stored_fields).toEqual(['@timestamp', 'bar']); + }); - test('removes created searchSource filter on removal', async () => { - const searchSource = new SearchSource({}, searchSourceDependencies); - searchSource.setField('index', indexPattern); - searchSource.setField('index', indexPattern2); - searchSource.setField('index', undefined); - const request = await searchSource.getSearchRequestBody(); - expect(request._source).toBe(undefined); + test('filters request when a specific list of fields is provided with fieldsFromSource or fields', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: ['*'], + scriptFields: { hello: {}, world: {} }, + docvalueFields: ['@timestamp', 'date', 'time'], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['hello', '@timestamp', 'foo-a', 'bar']); + searchSource.setField('fieldsFromSource', ['foo-b', 'date', 'baz']); + + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toEqual({ + includes: ['@timestamp', 'bar', 'date', 'baz'], + }); + expect(request.fields).toEqual(['hello', '@timestamp', 'bar', 'date']); + expect(request.script_fields).toEqual({ hello: {} }); + expect(request.stored_fields).toEqual(['@timestamp', 'bar', 'date', 'baz']); + }); + }); + + describe(`#setField('index')`, () => { + describe('auto-sourceFiltering', () => { + describe('new index pattern assigned', () => { + test('generates a searchSource filter', async () => { + expect(searchSource.getField('index')).toBe(undefined); + expect(searchSource.getField('source')).toBe(undefined); + searchSource.setField('index', indexPattern); + expect(searchSource.getField('index')).toBe(indexPattern); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource); + }); + + test('removes created searchSource filter on removal', async () => { + searchSource.setField('index', indexPattern); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + + describe('new index pattern assigned over another', () => { + test('replaces searchSource filter with new', async () => { + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + expect(searchSource.getField('index')).toBe(indexPattern2); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource2); + }); + + test('removes created searchSource filter on removal', async () => { + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); }); }); }); @@ -137,7 +470,7 @@ describe('SearchSource', () => { describe('#onRequestStart()', () => { test('should be called when starting a request', async () => { - const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const fn = jest.fn(); searchSource.onRequestStart(fn); const options = {}; @@ -147,7 +480,7 @@ describe('SearchSource', () => { test('should not be called on parent searchSource', async () => { const parent = new SearchSource({}, searchSourceDependencies); - const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const fn = jest.fn(); searchSource.onRequestStart(fn); @@ -162,12 +495,12 @@ describe('SearchSource', () => { test('should be called on parent searchSource if callParentStartHandlers is true', async () => { const parent = new SearchSource({}, searchSourceDependencies); - const searchSource = new SearchSource( - { index: indexPattern }, - searchSourceDependencies - ).setParent(parent, { - callParentStartHandlers: true, - }); + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies).setParent( + parent, + { + callParentStartHandlers: true, + } + ); const fn = jest.fn(); searchSource.onRequestStart(fn); @@ -192,7 +525,7 @@ describe('SearchSource', () => { }); test('should call msearch', async () => { - const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; await searchSource.fetch(options); expect(fetchSoon).toBeCalledTimes(1); @@ -201,7 +534,7 @@ describe('SearchSource', () => { describe('#search service fetch()', () => { test('should call msearch', async () => { - const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; await searchSource.fetch(options); @@ -212,7 +545,6 @@ describe('SearchSource', () => { describe('#serialize', () => { test('should reference index patterns', () => { const indexPattern123 = { id: '123' } as IndexPattern; - const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern123); const { searchSourceJSON, references } = searchSource.serialize(); expect(references[0].id).toEqual('123'); @@ -221,7 +553,6 @@ describe('SearchSource', () => { }); test('should add other fields', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('highlightAll', true); searchSource.setField('from', 123456); const { searchSourceJSON } = searchSource.serialize(); @@ -230,7 +561,6 @@ describe('SearchSource', () => { }); test('should omit sort and size', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('highlightAll', true); searchSource.setField('from', 123456); searchSource.setField('sort', { field: SortDirection.asc }); @@ -240,7 +570,6 @@ describe('SearchSource', () => { }); test('should serialize filters', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); const filter = [ { query: 'query', @@ -257,7 +586,6 @@ describe('SearchSource', () => { }); test('should reference index patterns in filters separately from index field', () => { - const searchSource = new SearchSource({}, searchSourceDependencies); const indexPattern123 = { id: '123' } as IndexPattern; searchSource.setField('index', indexPattern123); const filter = [ diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 79ef3a3f11ca5..2206d6d2816e2 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -70,14 +70,18 @@ */ import { setWith } from '@elastic/safer-lodash-set'; -import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; +import { uniqueId, keyBy, pick, difference, omit, isObject, isFunction } from 'lodash'; import { map } from 'rxjs/operators'; import { normalizeSortRequest } from './normalize_sort_request'; -import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern } from '../../index_patterns'; import { ISearchGeneric, ISearchOptions } from '../..'; -import type { ISearchSource, SearchSourceOptions, SearchSourceFields } from './types'; +import type { + ISearchSource, + SearchFieldValue, + SearchSourceOptions, + SearchSourceFields, +} from './types'; import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common'; @@ -404,7 +408,11 @@ export class SearchSource { case 'query': return addToRoot(key, (data[key] || []).concat(val)); case 'fields': - const fields = uniq((data[key] || []).concat(val)); + // uses new Fields API + return addToBody('fields', val); + case 'fieldsFromSource': + // preserves legacy behavior + const fields = [...new Set((data[key] || []).concat(val))]; return addToRoot(key, fields); case 'index': case 'type': @@ -451,49 +459,127 @@ export class SearchSource { } private flatten() { + const { getConfig } = this.dependencies; const searchRequest = this.mergeProps(); searchRequest.body = searchRequest.body || {}; - const { body, index, fields, query, filters, highlightAll } = searchRequest; + const { body, index, query, filters, highlightAll } = searchRequest; searchRequest.indexType = this.getIndexType(index); - const computedFields = index ? index.getComputedFields() : {}; - - body.stored_fields = computedFields.storedFields; - body.script_fields = body.script_fields || {}; - extend(body.script_fields, computedFields.scriptFields); - - const defaultDocValueFields = computedFields.docvalueFields - ? computedFields.docvalueFields - : []; - body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; - - if (!body.hasOwnProperty('_source') && index) { - body._source = index.getSourceFiltering(); + // get some special field types from the index pattern + const { docvalueFields, scriptFields, storedFields } = index + ? index.getComputedFields() + : { + docvalueFields: [], + scriptFields: {}, + storedFields: ['*'], + }; + + const fieldListProvided = !!body.fields; + const getFieldName = (fld: string | Record<string, any>): string => + typeof fld === 'string' ? fld : fld.field; + + // set defaults + let fieldsFromSource = searchRequest.fieldsFromSource || []; + body.fields = body.fields || []; + body.script_fields = { + ...body.script_fields, + ...scriptFields, + }; + body.stored_fields = storedFields; + + // apply source filters from index pattern if specified by the user + let filteredDocvalueFields = docvalueFields; + if (index) { + const sourceFilters = index.getSourceFiltering(); + if (!body.hasOwnProperty('_source')) { + body._source = sourceFilters; + } + if (body._source.excludes) { + const filter = fieldWildcardFilter( + body._source.excludes, + getConfig(UI_SETTINGS.META_FIELDS) + ); + // also apply filters to provided fields & default docvalueFields + body.fields = body.fields.filter((fld: SearchFieldValue) => filter(getFieldName(fld))); + fieldsFromSource = fieldsFromSource.filter((fld: SearchFieldValue) => + filter(getFieldName(fld)) + ); + filteredDocvalueFields = filteredDocvalueFields.filter((fld: SearchFieldValue) => + filter(getFieldName(fld)) + ); + } } - const { getConfig } = this.dependencies; + // specific fields were provided, so we need to exclude any others + if (fieldListProvided || fieldsFromSource.length) { + const bodyFieldNames = body.fields.map((field: string | Record<string, any>) => + getFieldName(field) + ); + const uniqFieldNames = [...new Set([...bodyFieldNames, ...fieldsFromSource])]; - if (body._source) { - // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, getConfig(UI_SETTINGS.META_FIELDS)); - body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => - filter(docvalueField.field) + // filter down script_fields to only include items specified + body.script_fields = pick( + body.script_fields, + Object.keys(body.script_fields).filter((f) => uniqFieldNames.includes(f)) ); - } - // if we only want to search for certain fields - if (fields) { - // filter out the docvalue_fields, and script_fields to only include those that we are concerned with - body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); - body.script_fields = pick(body.script_fields, fields); - - // request the remaining fields from both stored_fields and _source - const remainingFields = difference(fields, keys(body.script_fields)); - body.stored_fields = remainingFields; - setWith(body, '_source.includes', remainingFields, (nsValue) => - isObject(nsValue) ? {} : nsValue + // request the remaining fields from stored_fields just in case, since the + // fields API does not handle stored fields + const remainingFields = difference(uniqFieldNames, Object.keys(body.script_fields)).filter( + Boolean ); + + // only include unique values + body.stored_fields = [...new Set(remainingFields)]; + + if (fieldsFromSource.length) { + // include remaining fields in _source + setWith(body, '_source.includes', remainingFields, (nsValue) => + isObject(nsValue) ? {} : nsValue + ); + + // if items that are in the docvalueFields are provided, we should + // make sure those are added to the fields API unless they are + // already set in docvalue_fields + body.fields = [ + ...body.fields, + ...filteredDocvalueFields.filter((fld: SearchFieldValue) => { + return ( + fieldsFromSource.includes(getFieldName(fld)) && + !(body.docvalue_fields || []) + .map((d: string | Record<string, any>) => getFieldName(d)) + .includes(getFieldName(fld)) + ); + }), + ]; + + // delete fields array if it is still set to the empty default + if (!fieldListProvided && body.fields.length === 0) delete body.fields; + } else { + // remove _source, since everything's coming from fields API, scripted, or stored fields + body._source = false; + + // if items that are in the docvalueFields are provided, we should + // inject the format from the computed fields if one isn't given + const docvaluesIndex = keyBy(filteredDocvalueFields, 'field'); + body.fields = body.fields.map((fld: SearchFieldValue) => { + const fieldName = getFieldName(fld); + if (Object.keys(docvaluesIndex).includes(fieldName)) { + // either provide the field object from computed docvalues, + // or merge the user-provided field with the one in docvalues + return typeof fld === 'string' + ? docvaluesIndex[fld] + : { + ...docvaluesIndex[fieldName], + ...fld, + }; + } + return fld; + }); + } + } else { + body.fields = filteredDocvalueFields; } const esQueryConfigs = getEsQueryConfig({ get: getConfig }); diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 5fc747d454a01..c428dcf7fb484 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -59,6 +59,13 @@ export interface SortDirectionNumeric { export type EsQuerySortValue = Record<string, SortDirection | SortDirectionNumeric>; +interface SearchField { + [key: string]: SearchFieldValue; +} + +// @internal +export type SearchFieldValue = string | SearchField; + /** * search source fields */ @@ -86,7 +93,16 @@ export interface SearchSourceFields { size?: number; source?: NameList; version?: boolean; - fields?: NameList; + /** + * Retrieve fields via the search Fields API + */ + fields?: SearchFieldValue[]; + /** + * Retreive fields directly from _source (legacy behavior) + * + * @deprecated It is recommended to use `fields` wherever possible. + */ + fieldsFromSource?: NameList; /** * {@link IndexPatternService} */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ad1861cecea0b..484f07633e203 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2171,7 +2171,8 @@ export class SearchSource { size?: number | undefined; source?: string | boolean | string[] | undefined; version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; + fields?: SearchFieldValue[] | undefined; + fieldsFromSource?: string | boolean | string[] | undefined; index?: import("../..").IndexPattern | undefined; searchAfter?: import("./types").EsQuerySearchAfter | undefined; timeout?: string | undefined; @@ -2205,8 +2206,9 @@ export class SearchSource { export interface SearchSourceFields { // (undocumented) aggs?: any; - // (undocumented) - fields?: NameList; + fields?: SearchFieldValue[]; + // @deprecated + fieldsFromSource?: NameList; // (undocumented) filter?: Filter[] | Filter | (() => Filter[] | Filter | undefined); // (undocumented) @@ -2406,6 +2408,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:135:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/search_source/search_source.ts:197:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index 8ce9789d1dc84..b2aa3a05d7eb0 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -46,10 +46,8 @@ describe('getSharingData', () => { ], "searchRequest": Object { "body": Object { - "_source": Object { - "includes": Array [], - }, - "docvalue_fields": Array [], + "_source": Object {}, + "fields": undefined, "query": Object { "bool": Object { "filter": Array [], @@ -60,7 +58,7 @@ describe('getSharingData', () => { }, "script_fields": Object {}, "sort": Array [], - "stored_fields": Array [], + "stored_fields": undefined, }, "index": "the-index-pattern-title", }, diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index 0edaa356cba7d..e8844eb4eb6be 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -63,7 +63,7 @@ export async function getSharingData( index.timeFieldName || '', config.get(DOC_HIDE_TIME_COLUMN_SETTING) ); - searchSource.setField('fields', searchFields); + searchSource.setField('fieldsFromSource', searchFields); searchSource.setField( 'sort', getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING)) diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap index 92998bc3f07e3..8c4e05085528f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap @@ -8,7 +8,7 @@ exports[`AddFilter should ignore strings with just spaces 1`] = ` <EuiFieldText fullWidth={true} onChange={[Function]} - placeholder="source filter, accepts wildcards (e.g., \`user*\` to filter fields starting with 'user')" + placeholder="field filter, accepts wildcards (e.g., \`user*\` to filter fields starting with 'user')" value="" /> </EuiFlexItem> @@ -35,7 +35,7 @@ exports[`AddFilter should render normally 1`] = ` <EuiFieldText fullWidth={true} onChange={[Function]} - placeholder="source filter, accepts wildcards (e.g., \`user*\` to filter fields starting with 'user')" + placeholder="field filter, accepts wildcards (e.g., \`user*\` to filter fields starting with 'user')" value="" /> </EuiFlexItem> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx index 1d840743065a1..56e33b9e20f54 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx @@ -31,7 +31,7 @@ const sourcePlaceholder = i18n.translate( 'indexPatternManagement.editIndexPattern.sourcePlaceholder', { defaultMessage: - "source filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')", + "field filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')", } ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap index 0020adb19983d..9d92a3689b698 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -23,7 +23,7 @@ exports[`Header should render normally 1`] = ` onConfirm={[Function]} title={ <FormattedMessage - defaultMessage="Delete source filter '{value}'?" + defaultMessage="Delete field filter '{value}'?" id="indexPatternManagement.editIndexPattern.source.deleteSourceFilterLabel" values={ Object { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx index 28064cc839b80..bb90f6ca2c418 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx @@ -42,7 +42,7 @@ export const DeleteFilterConfirmationModal = ({ title={ <FormattedMessage id="indexPatternManagement.editIndexPattern.source.deleteSourceFilterLabel" - defaultMessage="Delete source filter '{value}'?" + defaultMessage="Delete field filter '{value}'?" values={{ value: filterToDeleteValue, }} diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap index 1f380d68a5af5..daa8e4a1c7063 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Header should render normally 1`] = ` > <h3> <FormattedMessage - defaultMessage="Source filters" + defaultMessage="Field filters" id="indexPatternManagement.editIndexPattern.sourceHeader" values={Object {}} /> @@ -16,7 +16,7 @@ exports[`Header should render normally 1`] = ` <EuiText> <p> <FormattedMessage - defaultMessage="Source filters can be used to exclude one or more fields when fetching the document source. This happens when viewing a document in the Discover app, or with a table displaying results from a saved search in the Dashboard app. Each row is built using the source of a single document, and if you have documents with large or unimportant fields you may benefit from filtering those out at this lower level." + defaultMessage="Field filters can be used to exclude one or more fields when fetching a document. This happens when viewing a document in the Discover app, or with a table displaying results from a saved search in the Dashboard app. If you have documents with large or unimportant fields you may benefit from filtering those out at this lower level." id="indexPatternManagement.editIndexPattern.sourceLabel" values={Object {}} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/header.tsx index 709908a1bb253..cf62ef86ade1b 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/header/header.tsx @@ -28,7 +28,7 @@ export const Header = () => ( <h3> <FormattedMessage id="indexPatternManagement.editIndexPattern.sourceHeader" - defaultMessage="Source filters" + defaultMessage="Field filters" /> </h3> </EuiTitle> @@ -36,10 +36,9 @@ export const Header = () => ( <p> <FormattedMessage id="indexPatternManagement.editIndexPattern.sourceLabel" - defaultMessage="Source filters can be used to exclude one or more fields when fetching the document source. This happens when - viewing a document in the Discover app, or with a table displaying results from a saved search in the Dashboard app. Each row is - built using the source of a single document, and if you have documents with large or unimportant fields you may benefit from - filtering those out at this lower level." + defaultMessage="Field filters can be used to exclude one or more fields when fetching a document. This happens when + viewing a document in the Discover app, or with a table displaying results from a saved search in the Dashboard app. + If you have documents with large or unimportant fields you may benefit from filtering those out at this lower level." /> </p> <p> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index a94ed60b7aed5..ed51fc3be5962 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -67,7 +67,7 @@ function getTitle(type: string, filteredCount: Dictionary<number>, totalCount: D break; case 'sourceFilters': title = i18n.translate('indexPatternManagement.editIndexPattern.tabs.sourceHeader', { - defaultMessage: 'Source filters', + defaultMessage: 'Field filters', }); break; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index ec14a80ae761e..3f8b9d3e28e1a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -113,7 +113,7 @@ describe('ESSearchSource', () => { }); const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters); expect(urlTemplateWithMeta.urlTemplate).toBe( - `rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fields,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape` + `rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape` ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 1c0645ae797ec..5a923f0ce4292 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -375,7 +375,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye maxResultWindow, initialSearchContext ); - searchSource.setField('fields', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields + searchSource.setField('fieldsFromSource', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields if (sourceOnlyFields.length === 0) { searchSource.setField('source', false); // do not need anything from _source } else { @@ -505,7 +505,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }; searchSource.setField('query', query); - searchSource.setField('fields', this._getTooltipPropertyNames()); + searchSource.setField('fieldsFromSource', this._getTooltipPropertyNames()); const resp = await searchSource.fetch(); @@ -708,7 +708,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye indexSettings.maxResultWindow, initialSearchContext ); - searchSource.setField('fields', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields + searchSource.setField('fieldsFromSource', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields if (sourceOnlyFields.length === 0) { searchSource.setField('source', false); // do not need anything from _source } else { diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index 6d86b93c3ec44..3de2f461bc855 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -32,7 +32,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect(mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0]).to.equal( - "/api/maps/mvt/getGridTile?x={x}&y={y}&z={z}&geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),docvalue_fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point" + "/api/maps/mvt/getGridTile?x={x}&y={y}&z={z}&geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point" ); //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) From eb0569b1fff5debad27fa636366970bac6703fa6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris <github@gidi.io> Date: Thu, 3 Dec 2020 15:13:59 +0000 Subject: [PATCH 102/107] [Alerting][Event Log] ensures we wait for the right number of events in test (#84189) Keeps the exact same assertions, but ensures the retry loop waits for them to complete so we don't assert too soon. --- .../common/lib/get_event_log.ts | 53 +++++++++++++++---- .../tests/actions/execute.ts | 4 +- .../tests/alerting/alerts.ts | 4 +- .../tests/alerting/event_log.ts | 2 +- .../spaces_only/tests/actions/execute.ts | 4 +- .../spaces_only/tests/alerting/event_log.ts | 28 ++++------ .../alerting/get_alert_instance_summary.ts | 2 +- 7 files changed, 59 insertions(+), 38 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts index aebcd854514b2..6336d834c3943 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -8,13 +8,26 @@ import { IValidatedEvent } from '../../../../plugins/event_log/server'; import { getUrlPrefix } from '.'; import { FtrProviderContext } from '../ftr_provider_context'; +interface GreaterThanEqualCondition { + gte: number; +} +interface EqualCondition { + equal: number; +} + +function isEqualConsition( + condition: GreaterThanEqualCondition | EqualCondition +): condition is EqualCondition { + return Number.isInteger((condition as EqualCondition).equal); +} + interface GetEventLogParams { getService: FtrProviderContext['getService']; spaceId: string; type: string; id: string; provider: string; - actions: string[]; + actions: Map<string, { gte: number } | { equal: number }>; } // Return event log entries given the specified parameters; for the `actions` @@ -22,7 +35,6 @@ interface GetEventLogParams { export async function getEventLog(params: GetEventLogParams): Promise<IValidatedEvent[]> { const { getService, spaceId, type, id, provider, actions } = params; const supertest = getService('supertest'); - const actionsSet = new Set(actions); const spacePrefix = getUrlPrefix(spaceId); const url = `${spacePrefix}/api/event_log/${type}/${id}/_find?per_page=5000`; @@ -36,14 +48,35 @@ export async function getEventLog(params: GetEventLogParams): Promise<IValidated const events: IValidatedEvent[] = (result.data as IValidatedEvent[]) .filter((event) => event?.event?.provider === provider) .filter((event) => event?.event?.action) - .filter((event) => actionsSet.has(event?.event?.action!)); - const foundActions = new Set( - events.map((event) => event?.event?.action).filter((action) => !!action) - ); - - for (const action of actions) { - if (!foundActions.has(action)) { - throw new Error(`no event found with action "${action}"`); + .filter((event) => actions.has(event?.event?.action!)); + + const foundActions = events + .map((event) => event?.event?.action) + .reduce((actionsSum, action) => { + if (action) { + actionsSum.set(action, 1 + (actionsSum.get(action) ?? 0)); + } + return actionsSum; + }, new Map<string, number>()); + + for (const [action, condition] of actions.entries()) { + if ( + !( + foundActions.has(action) && + (isEqualConsition(condition) + ? foundActions.get(action)! === condition.equal + : foundActions.get(action)! >= condition.gte) + ) + ) { + throw new Error( + `insufficient events found with action "${action}" (${ + foundActions.get(action) ?? 0 + } must be ${ + isEqualConsition(condition) + ? `equal to ${condition.equal}` + : `greater than or equal to ${condition.gte}` + })` + ); } } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 5c4eb5f5d4c54..9a3b2e7c137a4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -518,12 +518,10 @@ export default function ({ getService }: FtrProviderContext) { type: 'action', id: actionId, provider: 'actions', - actions: ['execute'], + actions: new Map([['execute', { equal: 1 }]]), }); }); - expect(events.length).to.equal(1); - const event = events[0]; const duration = event?.event?.duration; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 0820b7642e99e..ba21df286fe6e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -1096,12 +1096,10 @@ instanceStateValue: true type: 'alert', id: alertId, provider: 'alerting', - actions: ['execute'], + actions: new Map([['execute', { gte: 1 }]]), }); }); - expect(events.length).to.be.greaterThan(0); - const event = events[0]; const duration = event?.event?.duration; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts index 385d8bfca4a9a..459d214c8c993 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -56,7 +56,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { type: 'alert', id: alertId, provider: 'alerting', - actions: ['execute'], + actions: new Map([['execute', { gte: 1 }]]), }); const errorEvents = someEvents.filter( (event) => event?.kibana?.alerting?.status === 'error' diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 2316585d2d0f4..18ac7bfce4a69 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -270,12 +270,10 @@ export default function ({ getService }: FtrProviderContext) { type: 'action', id: actionId, provider: 'actions', - actions: ['execute'], + actions: new Map([['execute', { equal: 1 }]]), }); }); - expect(events.length).to.equal(1); - const event = events[0]; const duration = event?.event?.duration; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 3766785680925..6d43c28138457 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -17,8 +17,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/81668 - describe.skip('eventLog', () => { + describe('eventLog', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); @@ -73,27 +72,22 @@ export default function eventLogTests({ getService }: FtrProviderContext) { type: 'alert', id: alertId, provider: 'alerting', - actions: [ - 'execute', - 'execute-action', - 'new-instance', - 'active-instance', - 'recovered-instance', - ], + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute', { gte: 4 }], + ['execute-action', { equal: 2 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 1 }], + ['recovered-instance', { equal: 1 }], + ]), }); }); - // make sure the counts of the # of events per type are as expected const executeEvents = getEventsByAction(events, 'execute'); const executeActionEvents = getEventsByAction(events, 'execute-action'); const newInstanceEvents = getEventsByAction(events, 'new-instance'); const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); - expect(executeEvents.length >= 4).to.be(true); - expect(executeActionEvents.length).to.be(2); - expect(newInstanceEvents.length).to.be(1); - expect(recoveredInstanceEvents.length).to.be(1); - // make sure the events are in the right temporal order const executeTimes = getTimestamps(executeEvents); const executeActionTimes = getTimestamps(executeActionEvents); @@ -137,7 +131,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { validateInstanceEvent(event, `created new instance: 'instance'`); break; case 'recovered-instance': - validateInstanceEvent(event, `recovered instance: 'instance'`); + validateInstanceEvent(event, `instance 'instance' has recovered`); break; case 'active-instance': validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); @@ -182,7 +176,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { type: 'alert', id: alertId, provider: 'alerting', - actions: ['execute'], + actions: new Map([['execute', { gte: 1 }]]), }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 404c6020fa237..a5791a900af7e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -256,7 +256,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr type: 'alert', id, provider: 'alerting', - actions, + actions: new Map(actions.map((action) => [action, { gte: 1 }])), }); }); } From d2fc976b09a21896acade63d1280d19956a27dce Mon Sep 17 00:00:00 2001 From: Luke Elmers <luke.elmers@elastic.co> Date: Thu, 3 Dec 2020 08:28:50 -0700 Subject: [PATCH 103/107] [data.search][data.indexPatterns] Expose esaggs + indexPatternLoad on the server. (#84590) --- .../kibana-plugin-plugins-data-public.md | 2 +- ...blic.searchsessioninfoprovider.getname.md} | 2 +- ...essioninfoprovider.geturlgeneratordata.md} | 2 +- ...-data-public.searchsessioninfoprovider.md} | 6 +- ...gins-data-public.searchsource.getfields.md | 4 +- ...lugins-data-server.indexpatternsservice.md | 2 +- ...-data-server.indexpatternsservice.setup.md | 5 +- ...ublic.executioncontext.getkibanarequest.md | 13 ++ ...ins-expressions-public.executioncontext.md | 1 + ...erver.executioncontext.getkibanarequest.md | 13 ++ ...ins-expressions-server.executioncontext.md | 1 + .../field_formats/field_formats_registry.ts | 5 +- .../expressions/index.ts} | 22 +-- .../expressions/load_index_pattern.ts | 65 ++++++++ .../data/common/search/aggs/agg_type.test.ts | 11 ++ .../data/common/search/aggs/agg_type.ts | 5 +- .../search/aggs/buckets/date_histogram.ts | 2 + .../search/aggs/buckets/date_histogram_fn.ts | 11 +- .../search/aggs/buckets/date_range.test.ts | 25 +++ .../common/search/aggs/buckets/date_range.ts | 2 + .../search/aggs/buckets/date_range_fn.ts | 11 +- .../data/common/search/aggs/buckets/filter.ts | 2 + .../common/search/aggs/buckets/filter_fn.ts | 11 +- .../search/aggs/buckets/filters.test.ts | 27 +++ .../common/search/aggs/buckets/filters.ts | 2 + .../common/search/aggs/buckets/filters_fn.ts | 11 +- .../search/aggs/buckets/geo_hash.test.ts | 36 ++++ .../common/search/aggs/buckets/geo_hash.ts | 2 + .../common/search/aggs/buckets/geo_hash_fn.ts | 11 +- .../common/search/aggs/buckets/geo_tile.ts | 2 + .../common/search/aggs/buckets/geo_tile_fn.ts | 11 +- .../search/aggs/buckets/histogram.test.ts | 44 +++++ .../common/search/aggs/buckets/histogram.ts | 2 + .../search/aggs/buckets/histogram_fn.ts | 11 +- .../common/search/aggs/buckets/ip_range.ts | 2 + .../common/search/aggs/buckets/ip_range_fn.ts | 11 +- .../common/search/aggs/buckets/range.test.ts | 27 +++ .../data/common/search/aggs/buckets/range.ts | 2 + .../common/search/aggs/buckets/range_fn.ts | 11 +- .../search/aggs/buckets/shard_delay.test.ts | 21 +++ .../aggs/buckets/significant_terms.test.ts | 32 ++++ .../search/aggs/buckets/significant_terms.ts | 2 + .../aggs/buckets/significant_terms_fn.ts | 11 +- .../common/search/aggs/buckets/terms.test.ts | 74 +++++++++ .../data/common/search/aggs/buckets/terms.ts | 3 +- .../common/search/aggs/buckets/terms_fn.ts | 11 +- .../data/common/search/aggs/metrics/avg.ts | 2 + .../data/common/search/aggs/metrics/avg_fn.ts | 6 +- .../common/search/aggs/metrics/bucket_avg.ts | 2 + .../search/aggs/metrics/bucket_avg_fn.ts | 11 +- .../common/search/aggs/metrics/bucket_max.ts | 2 + .../search/aggs/metrics/bucket_max_fn.ts | 11 +- .../common/search/aggs/metrics/bucket_min.ts | 2 + .../search/aggs/metrics/bucket_min_fn.ts | 11 +- .../common/search/aggs/metrics/bucket_sum.ts | 2 + .../search/aggs/metrics/bucket_sum_fn.ts | 11 +- .../common/search/aggs/metrics/cardinality.ts | 2 + .../search/aggs/metrics/cardinality_fn.ts | 11 +- .../data/common/search/aggs/metrics/count.ts | 2 + .../common/search/aggs/metrics/count_fn.ts | 11 +- .../search/aggs/metrics/cumulative_sum.ts | 2 + .../search/aggs/metrics/cumulative_sum_fn.ts | 11 +- .../common/search/aggs/metrics/derivative.ts | 2 + .../search/aggs/metrics/derivative_fn.ts | 11 +- .../common/search/aggs/metrics/geo_bounds.ts | 2 + .../search/aggs/metrics/geo_bounds_fn.ts | 11 +- .../search/aggs/metrics/geo_centroid.ts | 2 + .../search/aggs/metrics/geo_centroid_fn.ts | 11 +- .../data/common/search/aggs/metrics/max.ts | 2 + .../data/common/search/aggs/metrics/max_fn.ts | 6 +- .../common/search/aggs/metrics/median.test.ts | 24 +++ .../data/common/search/aggs/metrics/median.ts | 2 + .../common/search/aggs/metrics/median_fn.ts | 11 +- .../data/common/search/aggs/metrics/min.ts | 2 + .../data/common/search/aggs/metrics/min_fn.ts | 6 +- .../common/search/aggs/metrics/moving_avg.ts | 2 + .../search/aggs/metrics/moving_avg_fn.ts | 11 +- .../aggs/metrics/percentile_ranks.test.ts | 60 ++++++- .../search/aggs/metrics/percentile_ranks.ts | 2 + .../aggs/metrics/percentile_ranks_fn.ts | 11 +- .../search/aggs/metrics/percentiles.test.ts | 32 ++++ .../common/search/aggs/metrics/percentiles.ts | 2 + .../search/aggs/metrics/percentiles_fn.ts | 11 +- .../common/search/aggs/metrics/serial_diff.ts | 2 + .../search/aggs/metrics/serial_diff_fn.ts | 11 +- .../search/aggs/metrics/std_deviation.test.ts | 25 +++ .../search/aggs/metrics/std_deviation.ts | 2 + .../search/aggs/metrics/std_deviation_fn.ts | 11 +- .../data/common/search/aggs/metrics/sum.ts | 2 + .../data/common/search/aggs/metrics/sum_fn.ts | 6 +- .../search/aggs/metrics/top_hit.test.ts | 36 ++++ .../common/search/aggs/metrics/top_hit.ts | 2 + .../common/search/aggs/metrics/top_hit_fn.ts | 11 +- .../esaggs/build_tabular_inspector_data.ts | 7 +- .../expressions/esaggs}/create_filter.test.ts | 14 +- .../expressions/esaggs}/create_filter.ts | 4 +- .../search/expressions/esaggs/esaggs_fn.ts | 154 +++++++++++++++++ .../search/expressions/esaggs/index.ts | 0 .../expressions/esaggs/request_handler.ts | 3 +- .../expressions/load_index_pattern.test.ts | 31 ++-- .../expressions/load_index_pattern.ts | 94 ++++++----- src/plugins/data/public/plugin.ts | 20 +-- src/plugins/data/public/public.api.md | 2 +- .../data/public/search/expressions/esaggs.ts | 115 +++++++++++++ .../search/expressions/esaggs/esaggs_fn.ts | 155 ------------------ .../data/public/search/expressions/index.ts | 2 +- .../data/public/search/search_service.ts | 16 +- .../index_patterns/expressions/index.ts | 20 +++ .../expressions/load_index_pattern.test.ts | 55 +++++++ .../expressions/load_index_pattern.ts | 100 +++++++++++ .../index_patterns/index_patterns_service.ts | 14 +- src/plugins/data/server/plugin.ts | 4 +- .../data/server/search/expressions/esaggs.ts | 136 +++++++++++++++ .../data/server/search/expressions/index.ts | 20 +++ .../data/server/search/search_service.ts | 2 + src/plugins/data/server/server.api.md | 10 +- .../common/execution/execution.test.ts | 12 ++ .../expressions/common/execution/execution.ts | 3 + .../expressions/common/execution/types.ts | 10 ++ .../common/service/expressions_services.ts | 10 ++ src/plugins/expressions/public/public.api.md | 2 + src/plugins/expressions/server/server.api.md | 2 + 122 files changed, 1652 insertions(+), 384 deletions(-) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md} (83%) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md} (82%) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.md} (84%) create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md create mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md rename src/plugins/data/common/{search/expressions/esaggs.ts => index_patterns/expressions/index.ts} (60%) create mode 100644 src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts rename src/plugins/data/{public => common}/search/expressions/esaggs/build_tabular_inspector_data.ts (95%) rename src/plugins/data/{public/search/expressions => common/search/expressions/esaggs}/create_filter.test.ts (91%) rename src/plugins/data/{public/search/expressions => common/search/expressions/esaggs}/create_filter.ts (94%) create mode 100644 src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts rename src/plugins/data/{public => common}/search/expressions/esaggs/index.ts (100%) rename src/plugins/data/{public => common}/search/expressions/esaggs/request_handler.ts (99%) create mode 100644 src/plugins/data/public/search/expressions/esaggs.ts delete mode 100644 src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts create mode 100644 src/plugins/data/server/index_patterns/expressions/index.ts create mode 100644 src/plugins/data/server/index_patterns/expressions/load_index_pattern.test.ts create mode 100644 src/plugins/data/server/index_patterns/expressions/load_index_pattern.ts create mode 100644 src/plugins/data/server/search/expressions/esaggs.ts create mode 100644 src/plugins/data/server/search/expressions/index.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 9121b0aade470..08ed14b92d24c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -89,7 +89,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | -| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md similarity index 83% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md index 0f0b616066dd6..2a5e1d2a3135f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md @@ -1,6 +1,6 @@ <!-- Do not edit this file. It is automatically generated by API Documenter. --> -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) ## SearchSessionInfoProvider.getName property diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md similarity index 82% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md index 207adaf2bd50b..01558ed3dddad 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md @@ -1,6 +1,6 @@ <!-- Do not edit this file. It is automatically generated by API Documenter. --> -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) ## SearchSessionInfoProvider.getUrlGeneratorData property diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md similarity index 84% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md index a3d294f5e3303..bcc4a5508eb59 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md @@ -1,6 +1,6 @@ <!-- Do not edit this file. It is automatically generated by API Documenter. --> -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) ## SearchSessionInfoProvider interface @@ -16,6 +16,6 @@ export interface SearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGenera | Property | Type | Description | | --- | --- | --- | -| [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) | <code>() => Promise<string></code> | User-facing name of the session. e.g. will be displayed in background sessions management list | -| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | <code>() => Promise<{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }></code> | | +| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | <code>() => Promise<string></code> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | <code>() => Promise<{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }></code> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md index 13b97ab4121c8..faff901bfc167 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md @@ -13,7 +13,7 @@ getFields(): { type?: string | undefined; query?: import("../..").Query | undefined; filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric> | Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric>[] | undefined; + sort?: Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection> | Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection>[] | undefined; highlight?: any; highlightAll?: boolean | undefined; aggs?: any; @@ -35,7 +35,7 @@ getFields(): { type?: string | undefined; query?: import("../..").Query | undefined; filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric> | Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric>[] | undefined; + sort?: Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection> | Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection>[] | undefined; highlight?: any; highlightAll?: boolean | undefined; aggs?: any; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index aa78c055f4f5c..439f4ff9fa78d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -14,6 +14,6 @@ export declare class IndexPatternsService implements Plugin<void, IndexPatternsS | Method | Modifiers | Description | | --- | --- | --- | -| [setup(core)](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | +| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | | [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md index a354fbc2a477b..6cac0a806d2ec 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md @@ -7,14 +7,15 @@ <b>Signature:</b> ```typescript -setup(core: CoreSetup): void; +setup(core: CoreSetup<DataPluginStartDependencies, DataPluginStart>, { expressions }: IndexPatternsServiceSetupDeps): void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| core | <code>CoreSetup</code> | | +| core | <code>CoreSetup<DataPluginStartDependencies, DataPluginStart></code> | | +| { expressions } | <code>IndexPatternsServiceSetupDeps</code> | | <b>Returns:</b> diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md new file mode 100644 index 0000000000000..a731d08a0d694 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) + +## ExecutionContext.getKibanaRequest property + +Getter to retrieve the `KibanaRequest` object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. + +<b>Signature:</b> + +```typescript +getKibanaRequest?: () => KibanaRequest; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 86d24534f7a44..1c0d10a382abf 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -17,6 +17,7 @@ export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters, | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-expressions-public.executioncontext.abortsignal.md) | <code>AbortSignal</code> | Adds ability to abort current execution. | +| [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | <code>() => KibanaRequest</code> | Getter to retrieve the <code>KibanaRequest</code> object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSavedObject](./kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md) | <code><T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>></code> | Allows to fetch saved objects from ElasticSearch. In browser <code>getSavedObject</code> function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. | | [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | <code>() => ExecutionContextSearch</code> | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | <code>() => string | undefined</code> | Search context in which expression should operate. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md new file mode 100644 index 0000000000000..203794a9d0302 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) + +## ExecutionContext.getKibanaRequest property + +Getter to retrieve the `KibanaRequest` object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. + +<b>Signature:</b> + +```typescript +getKibanaRequest?: () => KibanaRequest; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index e2547cc9470d1..fbf9dc634d563 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -17,6 +17,7 @@ export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters, | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-expressions-server.executioncontext.abortsignal.md) | <code>AbortSignal</code> | Adds ability to abort current execution. | +| [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | <code>() => KibanaRequest</code> | Getter to retrieve the <code>KibanaRequest</code> object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSavedObject](./kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md) | <code><T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>></code> | Allows to fetch saved objects from ElasticSearch. In browser <code>getSavedObject</code> function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. | | [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | <code>() => ExecutionContextSearch</code> | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | <code>() => string | undefined</code> | Search context in which expression should operate. | diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index dbc3693c99779..c7e99821d24c7 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -27,11 +27,10 @@ import { FieldFormatInstanceType, FieldFormatId, IFieldFormatMetaParams, - IFieldFormat, } from './types'; import { baseFormatters } from './constants/base_formatters'; import { FieldFormat } from './field_format'; -import { SerializedFieldFormat } from '../../../expressions/common/types'; +import { FormatFactory } from './utils'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../kbn_field_types/types'; import { UI_SETTINGS } from '../constants'; import { FieldFormatNotFoundError } from '../field_formats'; @@ -42,7 +41,7 @@ export class FieldFormatsRegistry { protected metaParamsOptions: Record<string, any> = {}; protected getConfig?: FieldFormatsGetConfigFn; // overriden on the public contract - public deserialize: (mapping: SerializedFieldFormat) => IFieldFormat = () => { + public deserialize: FormatFactory = () => { return new (FieldFormat.from(identity))(); }; diff --git a/src/plugins/data/common/search/expressions/esaggs.ts b/src/plugins/data/common/index_patterns/expressions/index.ts similarity index 60% rename from src/plugins/data/common/search/expressions/esaggs.ts rename to src/plugins/data/common/index_patterns/expressions/index.ts index 47d97a81a67b1..fa37e3b216ac9 100644 --- a/src/plugins/data/common/search/expressions/esaggs.ts +++ b/src/plugins/data/common/index_patterns/expressions/index.ts @@ -17,24 +17,4 @@ * under the License. */ -import { Datatable, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { KibanaContext } from './kibana_context_type'; - -type Input = KibanaContext | null; -type Output = Promise<Datatable>; - -interface Arguments { - index: string; - metricsAtAllLevels: boolean; - partialRows: boolean; - includeFormatHints: boolean; - aggConfigs: string; - timeFields?: string[]; -} - -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition< - 'esaggs', - Input, - Arguments, - Output ->; +export * from './load_index_pattern'; diff --git a/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts new file mode 100644 index 0000000000000..4c1b56df6e864 --- /dev/null +++ b/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { IndexPatternsContract } from '../index_patterns'; +import { IndexPatternSpec } from '..'; + +const name = 'indexPatternLoad'; + +type Input = null; +type Output = Promise<{ type: 'index_pattern'; value: IndexPatternSpec }>; + +interface Arguments { + id: string; +} + +/** @internal */ +export interface IndexPatternLoadStartDependencies { + indexPatterns: IndexPatternsContract; +} + +export type IndexPatternLoadExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof name, + Input, + Arguments, + Output +>; + +export const getIndexPatternLoadMeta = (): Omit< + IndexPatternLoadExpressionFunctionDefinition, + 'fn' +> => ({ + name, + type: 'index_pattern', + inputTypes: ['null'], + help: i18n.translate('data.functions.indexPatternLoad.help', { + defaultMessage: 'Loads an index pattern', + }), + args: { + id: { + types: ['string'], + required: true, + help: i18n.translate('data.functions.indexPatternLoad.id.help', { + defaultMessage: 'index pattern id to load', + }), + }, + }, +}); diff --git a/src/plugins/data/common/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts index 16a5586858ab9..102ec70188562 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -33,6 +33,7 @@ describe('AggType Class', () => { test('assigns the config value to itself', () => { const config: AggTypeConfig = { name: 'name', + expressionName: 'aggName', title: 'title', }; @@ -48,6 +49,7 @@ describe('AggType Class', () => { const aggConfig = {} as IAggConfig; const config: AggTypeConfig = { name: 'name', + expressionName: 'aggName', title: 'title', makeLabel, }; @@ -65,6 +67,7 @@ describe('AggType Class', () => { const aggType = new AggType({ name: 'name', + expressionName: 'aggName', title: 'title', getResponseAggs: testConfig, getRequestAggs: testConfig, @@ -78,6 +81,7 @@ describe('AggType Class', () => { const aggConfig = {} as IAggConfig; const aggType = new AggType({ name: 'name', + expressionName: 'aggName', title: 'title', }); const responseAggs = aggType.getRequestAggs(aggConfig); @@ -90,6 +94,7 @@ describe('AggType Class', () => { test('defaults to AggParams object with JSON param', () => { const aggType = new AggType({ name: 'smart agg', + expressionName: 'aggSmart', title: 'title', }); @@ -102,6 +107,7 @@ describe('AggType Class', () => { test('disables json param', () => { const aggType = new AggType({ name: 'name', + expressionName: 'aggName', title: 'title', json: false, }); @@ -113,6 +119,7 @@ describe('AggType Class', () => { test('can disable customLabel', () => { const aggType = new AggType({ name: 'smart agg', + expressionName: 'aggSmart', title: 'title', customLabels: false, }); @@ -127,6 +134,7 @@ describe('AggType Class', () => { const aggType = new AggType({ name: 'bucketeer', + expressionName: 'aggBucketeer', title: 'title', params, }); @@ -153,6 +161,7 @@ describe('AggType Class', () => { } as unknown) as IAggConfig; const aggType = new AggType({ name: 'name', + expressionName: 'aggName', title: 'title', }); expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(` @@ -168,6 +177,7 @@ describe('AggType Class', () => { } as unknown) as IAggConfig; const aggType = new AggType({ name: 'name', + expressionName: 'aggName', title: 'title', }); expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(`Object {}`); @@ -186,6 +196,7 @@ describe('AggType Class', () => { const getSerializedFormat = jest.fn().mockReturnValue({ id: 'hello' }); const aggType = new AggType({ name: 'name', + expressionName: 'aggName', title: 'title', getSerializedFormat, }); diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index bf6fe11f746f9..78e8c2405c510 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -39,7 +39,7 @@ export interface AggTypeConfig< createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any; type?: string; dslName?: string; - expressionName?: string; + expressionName: string; makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); ordered?: any; hasNoDsl?: boolean; @@ -90,12 +90,11 @@ export class AggType< dslName: string; /** * the name of the expression function that this aggType represents. - * TODO: this should probably be a required field. * * @property name * @type {string} */ - expressionName?: string; + expressionName: string; /** * the user friendly name that will be shown in the ui for this aggType * diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index 694b03f660452..ba79a4264d603 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -27,6 +27,7 @@ import { intervalOptions, autoInterval, isAutoInterval } from './_interval_optio import { createFilterDateHistogram } from './create_filter/date_histogram'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggDateHistogramFnName } from './date_histogram_fn'; import { ExtendedBounds } from './lib/extended_bounds'; import { TimeBuckets } from './lib/time_buckets'; @@ -87,6 +88,7 @@ export const getDateHistogramBucketAgg = ({ }: DateHistogramBucketAggDependencies) => new BucketAggType<IBucketDateHistogramAggConfig>({ name: BUCKET_TYPES.DATE_HISTOGRAM, + expressionName: aggDateHistogramFnName, title: i18n.translate('data.search.aggs.buckets.dateHistogramTitle', { defaultMessage: 'Date Histogram', }), diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts index 1cc5b41fa6bb3..3e3895b7b50db 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggDateHistogram'; +export const aggDateHistogramFnName = 'aggDateHistogram'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_HISTOGRAM>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_HISTOGRAM>; type Arguments = Assign<AggArgs, { timeRange?: string; extended_bounds?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggDateHistogramFnName, + Input, + Arguments, + Output +>; export const aggDateHistogram = (): FunctionDefinition => ({ - name: fnName, + name: aggDateHistogramFnName, help: i18n.translate('data.search.aggs.function.buckets.dateHistogram.help', { defaultMessage: 'Generates a serialized agg config for a Histogram agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/date_range.test.ts b/src/plugins/data/common/search/aggs/buckets/date_range.test.ts index 66f8e269cd38d..3cd06cc06545d 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.test.ts @@ -74,6 +74,31 @@ describe('date_range params', () => { ); }; + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs(); + const dateRange = aggConfigs.aggs[0]; + expect(dateRange.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "date_range", + ], + "ranges": Array [ + "[{\\"from\\":\\"now-1w/w\\",\\"to\\":\\"now\\"}]", + ], + "schema": Array [ + "buckets", + ], + }, + "function": "aggDateRange", + "type": "function", + } + `); + }); + describe('getKey', () => { test('should return object', () => { const aggConfigs = getAggConfigs(); diff --git a/src/plugins/data/common/search/aggs/buckets/date_range.ts b/src/plugins/data/common/search/aggs/buckets/date_range.ts index f9a3acb990fbf..cb01922170664 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.ts @@ -24,6 +24,7 @@ import { i18n } from '@kbn/i18n'; import { BUCKET_TYPES } from './bucket_agg_types'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterDateRange } from './create_filter/date_range'; +import { aggDateRangeFnName } from './date_range_fn'; import { DateRangeKey } from './lib/date_range'; import { KBN_FIELD_TYPES } from '../../../../common/kbn_field_types/types'; @@ -50,6 +51,7 @@ export const getDateRangeBucketAgg = ({ }: DateRangeBucketAggDependencies) => new BucketAggType({ name: BUCKET_TYPES.DATE_RANGE, + expressionName: aggDateRangeFnName, title: dateRangeTitle, createFilter: createFilterDateRange, getKey({ from, to }): DateRangeKey { diff --git a/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts b/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts index 5027aadbb7331..0dc66be5b84f2 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggDateRange'; +export const aggDateRangeFnName = 'aggDateRange'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_RANGE>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_RANGE>; type Arguments = Assign<AggArgs, { ranges?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggDateRangeFnName, + Input, + Arguments, + Output +>; export const aggDateRange = (): FunctionDefinition => ({ - name: fnName, + name: aggDateRangeFnName, help: i18n.translate('data.search.aggs.function.buckets.dateRange.help', { defaultMessage: 'Generates a serialized agg config for a Date Range agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/filter.ts b/src/plugins/data/common/search/aggs/buckets/filter.ts index 5d146e125b996..84faaa2b360bd 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter.ts @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { GeoBoundingBox } from './lib/geo_point'; +import { aggFilterFnName } from './filter_fn'; import { BaseAggParams } from '../types'; const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', { @@ -34,6 +35,7 @@ export interface AggParamsFilter extends BaseAggParams { export const getFilterBucketAgg = () => new BucketAggType({ name: BUCKET_TYPES.FILTER, + expressionName: aggFilterFnName, title: filterTitle, makeLabel: () => filterTitle, params: [ diff --git a/src/plugins/data/common/search/aggs/buckets/filter_fn.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts index ae60da3e8a47c..8c8c0f430184a 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggFilter'; +export const aggFilterFnName = 'aggFilter'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>; type Arguments = Assign<AggArgs, { geo_bounding_box?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggFilterFnName, + Input, + Arguments, + Output +>; export const aggFilter = (): FunctionDefinition => ({ - name: fnName, + name: aggFilterFnName, help: i18n.translate('data.search.aggs.function.buckets.filter.help', { defaultMessage: 'Generates a serialized agg config for a Filter agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/filters.test.ts b/src/plugins/data/common/search/aggs/buckets/filters.test.ts index f745b4537131a..326a3af712e70 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.test.ts @@ -74,6 +74,33 @@ describe('Filters Agg', () => { }, }); + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs({ + filters: [ + generateFilter('a', 'lucene', 'foo'), + generateFilter('b', 'lucene', 'status:200'), + generateFilter('c', 'lucene', 'status:[400 TO 499] AND (foo OR bar)'), + ], + }); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "filters": Array [ + "[{\\"label\\":\\"a\\",\\"input\\":{\\"language\\":\\"lucene\\",\\"query\\":\\"foo\\"}},{\\"label\\":\\"b\\",\\"input\\":{\\"language\\":\\"lucene\\",\\"query\\":\\"status:200\\"}},{\\"label\\":\\"c\\",\\"input\\":{\\"language\\":\\"lucene\\",\\"query\\":\\"status:[400 TO 499] AND (foo OR bar)\\"}}]", + ], + "id": Array [ + "test", + ], + }, + "function": "aggFilters", + "type": "function", + } + `); + }); + describe('using Lucene', () => { test('works with lucene filters', () => { const aggConfigs = getAggConfigs({ diff --git a/src/plugins/data/common/search/aggs/buckets/filters.ts b/src/plugins/data/common/search/aggs/buckets/filters.ts index 7310fa08b68e0..7f43d01808882 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.ts @@ -24,6 +24,7 @@ import { createFilterFilters } from './create_filter/filters'; import { toAngularJSON } from '../utils'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggFiltersFnName } from './filters_fn'; import { getEsQueryConfig, buildEsQuery, Query, UI_SETTINGS } from '../../../../common'; import { BaseAggParams } from '../types'; @@ -53,6 +54,7 @@ export interface AggParamsFilters extends Omit<BaseAggParams, 'customLabel'> { export const getFiltersBucketAgg = ({ getConfig }: FiltersBucketAggDependencies) => new BucketAggType({ name: BUCKET_TYPES.FILTERS, + expressionName: aggFiltersFnName, title: filtersTitle, createFilter: createFilterFilters, customLabels: false, diff --git a/src/plugins/data/common/search/aggs/buckets/filters_fn.ts b/src/plugins/data/common/search/aggs/buckets/filters_fn.ts index 55380ea815315..194feb67d3366 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggFilters'; +export const aggFiltersFnName = 'aggFilters'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTERS>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTERS>; type Arguments = Assign<AggArgs, { filters?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggFiltersFnName, + Input, + Arguments, + Output +>; export const aggFilters = (): FunctionDefinition => ({ - name: fnName, + name: aggFiltersFnName, help: i18n.translate('data.search.aggs.function.buckets.filters.help', { defaultMessage: 'Generates a serialized agg config for a Filter agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts index e77d2bf1eaf5f..8de6834022639 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts @@ -87,6 +87,42 @@ describe('Geohash Agg', () => { }); }); + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs(); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "autoPrecision": Array [ + true, + ], + "enabled": Array [ + true, + ], + "field": Array [ + "location", + ], + "id": Array [ + "geohash_grid", + ], + "isFilteredByCollar": Array [ + true, + ], + "precision": Array [ + 2, + ], + "schema": Array [ + "segment", + ], + "useGeocentroid": Array [ + true, + ], + }, + "function": "aggGeoHash", + "type": "function", + } + `); + }); + describe('getRequestAggs', () => { describe('initial aggregation creation', () => { let aggConfigs: IAggConfigs; diff --git a/src/plugins/data/common/search/aggs/buckets/geo_hash.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash.ts index a0ef8a27b0d1e..b7ddf24dbfc84 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_hash.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_hash.ts @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggGeoHashFnName } from './geo_hash_fn'; import { GeoBoundingBox } from './lib/geo_point'; import { BaseAggParams } from '../types'; @@ -47,6 +48,7 @@ export interface AggParamsGeoHash extends BaseAggParams { export const getGeoHashBucketAgg = () => new BucketAggType<IBucketAggConfig>({ name: BUCKET_TYPES.GEOHASH_GRID, + expressionName: aggGeoHashFnName, title: geohashGridTitle, makeLabel: () => geohashGridTitle, params: [ diff --git a/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts index 5152804bf8122..aa5f473f73f9d 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts @@ -23,17 +23,22 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggGeoHash'; +export const aggGeoHashFnName = 'aggGeoHash'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.GEOHASH_GRID>; type Arguments = Assign<AggArgs, { boundingBox?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggGeoHashFnName, + Input, + Arguments, + Output +>; export const aggGeoHash = (): FunctionDefinition => ({ - name: fnName, + name: aggGeoHashFnName, help: i18n.translate('data.search.aggs.function.buckets.geoHash.help', { defaultMessage: 'Generates a serialized agg config for a Geo Hash agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/geo_tile.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile.ts index e6eff1e1a5d8e..fc87d632c7e9c 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_tile.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_tile.ts @@ -22,6 +22,7 @@ import { noop } from 'lodash'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggGeoTileFnName } from './geo_tile_fn'; import { KBN_FIELD_TYPES } from '../../../../common'; import { METRIC_TYPES } from '../metrics/metric_agg_types'; import { BaseAggParams } from '../types'; @@ -39,6 +40,7 @@ export interface AggParamsGeoTile extends BaseAggParams { export const getGeoTitleBucketAgg = () => new BucketAggType({ name: BUCKET_TYPES.GEOTILE_GRID, + expressionName: aggGeoTileFnName, title: geotileGridTitle, params: [ { diff --git a/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts index ed3142408892a..346c70bba31fd 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts @@ -22,16 +22,21 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggGeoTile'; +export const aggGeoTileFnName = 'aggGeoTile'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.GEOTILE_GRID>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggGeoTileFnName, + Input, + AggArgs, + Output +>; export const aggGeoTile = (): FunctionDefinition => ({ - name: fnName, + name: aggGeoTileFnName, help: i18n.translate('data.search.aggs.function.buckets.geoTile.help', { defaultMessage: 'Generates a serialized agg config for a Geo Tile agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index a8ac72c174c72..1b01b1f235cb5 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -72,6 +72,50 @@ describe('Histogram Agg', () => { return aggConfigs.aggs[0].toDsl()[BUCKET_TYPES.HISTOGRAM]; }; + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs({ + intervalBase: 100, + field: { + name: 'field', + }, + }); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "extended_bounds": Array [ + "{\\"min\\":\\"\\",\\"max\\":\\"\\"}", + ], + "field": Array [ + "field", + ], + "has_extended_bounds": Array [ + false, + ], + "id": Array [ + "test", + ], + "interval": Array [ + "auto", + ], + "intervalBase": Array [ + 100, + ], + "min_doc_count": Array [ + false, + ], + "schema": Array [ + "segment", + ], + }, + "function": "aggHistogram", + "type": "function", + } + `); + }); + describe('ordered', () => { let histogramType: BucketAggType<IBucketHistogramAggConfig>; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index c3d3f041dd0c7..ab0d566b273c7 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -27,6 +27,7 @@ import { BaseAggParams } from '../types'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggHistogramFnName } from './histogram_fn'; import { ExtendedBounds } from './lib/extended_bounds'; import { isAutoInterval, autoInterval } from './_interval_options'; import { calculateHistogramInterval } from './lib/histogram_calculate_interval'; @@ -62,6 +63,7 @@ export const getHistogramBucketAgg = ({ }: HistogramBucketAggDependencies) => new BucketAggType<IBucketHistogramAggConfig>({ name: BUCKET_TYPES.HISTOGRAM, + expressionName: aggHistogramFnName, title: i18n.translate('data.search.aggs.buckets.histogramTitle', { defaultMessage: 'Histogram', }), diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts index 2e833bbe0a3eb..153a7bfc1c592 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggHistogram'; +export const aggHistogramFnName = 'aggHistogram'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.HISTOGRAM>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.HISTOGRAM>; type Arguments = Assign<AggArgs, { extended_bounds?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggHistogramFnName, + Input, + Arguments, + Output +>; export const aggHistogram = (): FunctionDefinition => ({ - name: fnName, + name: aggHistogramFnName, help: i18n.translate('data.search.aggs.function.buckets.histogram.help', { defaultMessage: 'Generates a serialized agg config for a Histogram agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/ip_range.ts index d0a6174b011fc..233acdd71e59a 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range.ts @@ -24,6 +24,7 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterIpRange } from './create_filter/ip_range'; import { IpRangeKey, RangeIpRangeAggKey, CidrMaskIpRangeAggKey } from './lib/ip_range'; +import { aggIpRangeFnName } from './ip_range_fn'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BaseAggParams } from '../types'; @@ -48,6 +49,7 @@ export interface AggParamsIpRange extends BaseAggParams { export const getIpRangeBucketAgg = () => new BucketAggType({ name: BUCKET_TYPES.IP_RANGE, + expressionName: aggIpRangeFnName, title: ipRangeTitle, createFilter: createFilterIpRange, getKey(bucket, key, agg): IpRangeKey { diff --git a/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts index 15b763fd42d6b..7ad61a9c27d86 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggIpRange'; +export const aggIpRangeFnName = 'aggIpRange'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.IP_RANGE>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.IP_RANGE>; type Arguments = Assign<AggArgs, { ranges?: string; ipRangeType?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggIpRangeFnName, + Input, + Arguments, + Output +>; export const aggIpRange = (): FunctionDefinition => ({ - name: fnName, + name: aggIpRangeFnName, help: i18n.translate('data.search.aggs.function.buckets.ipRange.help', { defaultMessage: 'Generates a serialized agg config for a Ip Range agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/range.test.ts b/src/plugins/data/common/search/aggs/buckets/range.test.ts index b8241e04ea1ee..c878e6b81a0ae 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.test.ts @@ -66,6 +66,33 @@ describe('Range Agg', () => { ); }; + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs(); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "1", + ], + "ranges": Array [ + "[{\\"from\\":0,\\"to\\":1000},{\\"from\\":1000,\\"to\\":2000}]", + ], + "schema": Array [ + "segment", + ], + }, + "function": "aggRange", + "type": "function", + } + `); + }); + describe('getSerializedFormat', () => { test('generates a serialized field format in the expected shape', () => { const aggConfigs = getAggConfigs(); diff --git a/src/plugins/data/common/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts index bdb6ea7cd4b98..4486ad3c06dd1 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -24,6 +24,7 @@ import { AggTypesDependencies } from '../agg_types'; import { BaseAggParams } from '../types'; import { BucketAggType } from './bucket_agg_type'; +import { aggRangeFnName } from './range_fn'; import { RangeKey } from './range_key'; import { createFilterRange } from './create_filter/range'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -50,6 +51,7 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend return new BucketAggType({ name: BUCKET_TYPES.RANGE, + expressionName: aggRangeFnName, title: rangeTitle, createFilter: createFilterRange(getFieldFormatsStart), makeLabel(aggConfig) { diff --git a/src/plugins/data/common/search/aggs/buckets/range_fn.ts b/src/plugins/data/common/search/aggs/buckets/range_fn.ts index 6806125a10f6d..a52b2427b9845 100644 --- a/src/plugins/data/common/search/aggs/buckets/range_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/range_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggRange'; +export const aggRangeFnName = 'aggRange'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.RANGE>; @@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.RANGE>; type Arguments = Assign<AggArgs, { ranges?: string }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggRangeFnName, + Input, + Arguments, + Output +>; export const aggRange = (): FunctionDefinition => ({ - name: fnName, + name: aggRangeFnName, help: i18n.translate('data.search.aggs.function.buckets.range.help', { defaultMessage: 'Generates a serialized agg config for a Range agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts index 15399ffc43791..063dec97dadd4 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts @@ -60,6 +60,27 @@ describe('Shard Delay Agg', () => { ); }; + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs(); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "delay": Array [ + "5s", + ], + "enabled": Array [ + true, + ], + "id": Array [ + "1", + ], + }, + "function": "aggShardDelay", + "type": "function", + } + `); + }); + describe('write', () => { test('writes the delay as the value parameter', () => { const aggConfigs = getAggConfigs(); diff --git a/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts index e6c7bbee72a72..be40ff2267f11 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts @@ -64,6 +64,38 @@ describe('Significant Terms Agg', () => { expect(params.exclude).toBe('400'); }; + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs({ + size: 'SIZE', + field: { + name: 'FIELD', + }, + }); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "FIELD", + ], + "id": Array [ + "test", + ], + "schema": Array [ + "segment", + ], + "size": Array [ + "SIZE", + ], + }, + "function": "aggSignificantTerms", + "type": "function", + } + `); + }); + test('should generate correct label', () => { const aggConfigs = getAggConfigs({ size: 'SIZE', diff --git a/src/plugins/data/common/search/aggs/buckets/significant_terms.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms.ts index 4dc8aafd8a7a7..5632c08378f4c 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms.ts @@ -22,6 +22,7 @@ import { BucketAggType } from './bucket_agg_type'; import { createFilterTerms } from './create_filter/terms'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggSignificantTermsFnName } from './significant_terms_fn'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BaseAggParams } from '../types'; @@ -39,6 +40,7 @@ export interface AggParamsSignificantTerms extends BaseAggParams { export const getSignificantTermsBucketAgg = () => new BucketAggType({ name: BUCKET_TYPES.SIGNIFICANT_TERMS, + expressionName: aggSignificantTermsFnName, title: significantTermsTitle, makeLabel(aggConfig) { return i18n.translate('data.search.aggs.buckets.significantTermsLabel', { diff --git a/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts index 1fecfcc914313..a1a7500678fd6 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts @@ -22,7 +22,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggSignificantTerms'; +export const aggSignificantTermsFnName = 'aggSignificantTerms'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.SIGNIFICANT_TERMS>; @@ -30,10 +30,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.SIGNIFICANT_TERMS>; type Arguments = AggArgs; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggSignificantTermsFnName, + Input, + Arguments, + Output +>; export const aggSignificantTerms = (): FunctionDefinition => ({ - name: fnName, + name: aggSignificantTermsFnName, help: i18n.translate('data.search.aggs.function.buckets.significantTerms.help', { defaultMessage: 'Generates a serialized agg config for a Significant Terms agg', }), diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 8f645b4712c7f..a4116500bec12 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -52,6 +52,80 @@ describe('Terms Agg', () => { ); }; + test('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs({ + include: { + pattern: '404', + }, + exclude: { + pattern: '400', + }, + field: { + name: 'field', + }, + orderAgg: { + type: 'count', + }, + }); + expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "field", + ], + "id": Array [ + "test", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "desc", + ], + "orderAgg": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "test-orderAgg", + ], + "schema": Array [ + "orderAgg", + ], + }, + "function": "aggCount", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + } + `); + }); + test('converts object to string type', () => { const aggConfigs = getAggConfigs({ include: { diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 7071d9c1dc9c4..8683b23b39c85 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -28,6 +28,7 @@ import { isStringOrNumberType, migrateIncludeExcludeFormat, } from './migrate_include_exclude_format'; +import { aggTermsFnName } from './terms_fn'; import { AggConfigSerialized, BaseAggParams } from '../types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -75,7 +76,7 @@ export interface AggParamsTerms extends BaseAggParams { export const getTermsBucketAgg = () => new BucketAggType({ name: BUCKET_TYPES.TERMS, - expressionName: 'aggTerms', + expressionName: aggTermsFnName, title: termsTitle, makeLabel(agg) { const params = agg.params; diff --git a/src/plugins/data/common/search/aggs/buckets/terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/terms_fn.ts index 975941506da4e..7737cb1e1c952 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggTerms'; +export const aggTermsFnName = 'aggTerms'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.TERMS>; @@ -33,10 +33,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.TERMS>; type Arguments = Assign<AggArgs, { orderAgg?: AggExpressionType }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTermsFnName, + Input, + Arguments, + Output +>; export const aggTerms = (): FunctionDefinition => ({ - name: fnName, + name: aggTermsFnName, help: i18n.translate('data.search.aggs.function.buckets.terms.help', { defaultMessage: 'Generates a serialized agg config for a Terms agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/avg.ts b/src/plugins/data/common/search/aggs/metrics/avg.ts index 651aaf857c757..49c81b2918346 100644 --- a/src/plugins/data/common/search/aggs/metrics/avg.ts +++ b/src/plugins/data/common/search/aggs/metrics/avg.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggAvgFnName } from './avg_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -34,6 +35,7 @@ export interface AggParamsAvg extends BaseAggParams { export const getAvgMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.AVG, + expressionName: aggAvgFnName, title: averageTitle, makeLabel: (aggConfig) => { return i18n.translate('data.search.aggs.metrics.averageLabel', { diff --git a/src/plugins/data/common/search/aggs/metrics/avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/avg_fn.ts index 18629927d7814..57dd3dae70fba 100644 --- a/src/plugins/data/common/search/aggs/metrics/avg_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/avg_fn.ts @@ -22,15 +22,15 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggAvg'; +export const aggAvgFnName = 'aggAvg'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.AVG>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition<typeof aggAvgFnName, Input, AggArgs, Output>; export const aggAvg = (): FunctionDefinition => ({ - name: fnName, + name: aggAvgFnName, help: i18n.translate('data.search.aggs.function.metrics.avg.help', { defaultMessage: 'Generates a serialized agg config for a Avg agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_avg.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg.ts index 92fa675ac2d38..003627ddec2a1 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_avg.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_avg.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; +import { aggBucketAvgFnName } from './bucket_avg_fn'; import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; @@ -43,6 +44,7 @@ export const getBucketAvgMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.AVG_BUCKET, + expressionName: aggBucketAvgFnName, title: averageBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallAverageLabel), subtype, diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts index 4e0c1d7311cd6..595d49647d9c2 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggBucketAvg'; +export const aggBucketAvgFnName = 'aggBucketAvg'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.AVG_BUCKET>; @@ -32,10 +32,15 @@ type Arguments = Assign< { customBucket?: AggExpressionType; customMetric?: AggExpressionType } >; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggBucketAvgFnName, + Input, + Arguments, + Output +>; export const aggBucketAvg = (): FunctionDefinition => ({ - name: fnName, + name: aggBucketAvgFnName, help: i18n.translate('data.search.aggs.function.metrics.bucket_avg.help', { defaultMessage: 'Generates a serialized agg config for a Avg Bucket agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_max.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max.ts index 8e2606676ec33..c37e0d6e09e23 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_max.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_max.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggBucketMaxFnName } from './bucket_max_fn'; import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; @@ -42,6 +43,7 @@ export const getBucketMaxMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.MAX_BUCKET, + expressionName: aggBucketMaxFnName, title: maxBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallMaxLabel), subtype, diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts index 66ae7601470fb..482c73e7d3005 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggBucketMax'; +export const aggBucketMaxFnName = 'aggBucketMax'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.MAX_BUCKET>; @@ -32,10 +32,15 @@ type Arguments = Assign< { customBucket?: AggExpressionType; customMetric?: AggExpressionType } >; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggBucketMaxFnName, + Input, + Arguments, + Output +>; export const aggBucketMax = (): FunctionDefinition => ({ - name: fnName, + name: aggBucketMaxFnName, help: i18n.translate('data.search.aggs.function.metrics.bucket_max.help', { defaultMessage: 'Generates a serialized agg config for a Max Bucket agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_min.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min.ts index dedc3a9de3dd1..2aee271a69cc3 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_min.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_min.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggBucketMinFnName } from './bucket_min_fn'; import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; @@ -42,6 +43,7 @@ export const getBucketMinMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.MIN_BUCKET, + expressionName: aggBucketMinFnName, title: minBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallMinLabel), subtype, diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts index 009cc0102b05d..68beffbf05660 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggBucketMin'; +export const aggBucketMinFnName = 'aggBucketMin'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.MIN_BUCKET>; @@ -32,10 +32,15 @@ type Arguments = Assign< { customBucket?: AggExpressionType; customMetric?: AggExpressionType } >; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggBucketMinFnName, + Input, + Arguments, + Output +>; export const aggBucketMin = (): FunctionDefinition => ({ - name: fnName, + name: aggBucketMinFnName, help: i18n.translate('data.search.aggs.function.metrics.bucket_min.help', { defaultMessage: 'Generates a serialized agg config for a Min Bucket agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_sum.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum.ts index c6ccd498a0eb9..d7a7ed47ac2df 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_sum.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_sum.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggBucketSumFnName } from './bucket_sum_fn'; import { MetricAggType } from './metric_agg_type'; import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; @@ -42,6 +43,7 @@ export const getBucketSumMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.SUM_BUCKET, + expressionName: aggBucketSumFnName, title: sumBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallSumLabel), subtype, diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts index 920285e89e8f4..7994bb85be2a7 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts @@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggBucketSum'; +export const aggBucketSumFnName = 'aggBucketSum'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.SUM_BUCKET>; @@ -32,10 +32,15 @@ type Arguments = Assign< { customBucket?: AggExpressionType; customMetric?: AggExpressionType } >; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggBucketSumFnName, + Input, + Arguments, + Output +>; export const aggBucketSum = (): FunctionDefinition => ({ - name: fnName, + name: aggBucketSumFnName, help: i18n.translate('data.search.aggs.function.metrics.bucket_sum.help', { defaultMessage: 'Generates a serialized agg config for a Sum Bucket agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality.ts b/src/plugins/data/common/search/aggs/metrics/cardinality.ts index 777cb833849f4..91f2b729e9dda 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggCardinalityFnName } from './cardinality_fn'; import { MetricAggType, IMetricAggConfig } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -34,6 +35,7 @@ export interface AggParamsCardinality extends BaseAggParams { export const getCardinalityMetricAgg = () => new MetricAggType({ name: METRIC_TYPES.CARDINALITY, + expressionName: aggCardinalityFnName, title: uniqueCountTitle, makeLabel(aggConfig: IMetricAggConfig) { return i18n.translate('data.search.aggs.metrics.uniqueCountLabel', { diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts index 2542c76e7be57..6e78a42fea90f 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggCardinality'; +export const aggCardinalityFnName = 'aggCardinality'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.CARDINALITY>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggCardinalityFnName, + Input, + AggArgs, + Output +>; export const aggCardinality = (): FunctionDefinition => ({ - name: fnName, + name: aggCardinalityFnName, help: i18n.translate('data.search.aggs.function.metrics.cardinality.help', { defaultMessage: 'Generates a serialized agg config for a Cardinality agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index 9c9f36651f4d2..a50b627ae2398 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -18,12 +18,14 @@ */ import { i18n } from '@kbn/i18n'; +import { aggCountFnName } from './count_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; export const getCountMetricAgg = () => new MetricAggType({ name: METRIC_TYPES.COUNT, + expressionName: aggCountFnName, title: i18n.translate('data.search.aggs.metrics.countTitle', { defaultMessage: 'Count', }), diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index 7d4616ffdc619..a4df6f9ebd061 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -21,15 +21,20 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -const fnName = 'aggCount'; +export const aggCountFnName = 'aggCount'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.COUNT>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggCountFnName, + Input, + AggArgs, + Output +>; export const aggCount = (): FunctionDefinition => ({ - name: fnName, + name: aggCountFnName, help: i18n.translate('data.search.aggs.function.metrics.count.help', { defaultMessage: 'Generates a serialized agg config for a Count agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts index b10bdd31a5817..bb0d15782c342 100644 --- a/src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts +++ b/src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggCumulativeSumFnName } from './cumulative_sum_fn'; import { MetricAggType } from './metric_agg_type'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; @@ -43,6 +44,7 @@ export const getCumulativeSumMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.CUMULATIVE_SUM, + expressionName: aggCumulativeSumFnName, title: cumulativeSumTitle, makeLabel: (agg) => makeNestedLabel(agg, cumulativeSumLabel), subtype, diff --git a/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts index 411cbd256c37e..43df5301e1a04 100644 --- a/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts @@ -23,16 +23,21 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggCumulativeSum'; +export const aggCumulativeSumFnName = 'aggCumulativeSum'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.CUMULATIVE_SUM>; type Arguments = Assign<AggArgs, { customMetric?: AggExpressionType }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggCumulativeSumFnName, + Input, + Arguments, + Output +>; export const aggCumulativeSum = (): FunctionDefinition => ({ - name: fnName, + name: aggCumulativeSumFnName, help: i18n.translate('data.search.aggs.function.metrics.cumulative_sum.help', { defaultMessage: 'Generates a serialized agg config for a Cumulative Sum agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/derivative.ts b/src/plugins/data/common/search/aggs/metrics/derivative.ts index c03c33ba80710..ee32d12e5c85d 100644 --- a/src/plugins/data/common/search/aggs/metrics/derivative.ts +++ b/src/plugins/data/common/search/aggs/metrics/derivative.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggDerivativeFnName } from './derivative_fn'; import { MetricAggType } from './metric_agg_type'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; @@ -43,6 +44,7 @@ export const getDerivativeMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.DERIVATIVE, + expressionName: aggDerivativeFnName, title: derivativeTitle, makeLabel(agg) { return makeNestedLabel(agg, derivativeLabel); diff --git a/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts b/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts index 1d87dfdac6da3..354166ad728ad 100644 --- a/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts @@ -23,16 +23,21 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggDerivative'; +export const aggDerivativeFnName = 'aggDerivative'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.DERIVATIVE>; type Arguments = Assign<AggArgs, { customMetric?: AggExpressionType }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggDerivativeFnName, + Input, + Arguments, + Output +>; export const aggDerivative = (): FunctionDefinition => ({ - name: fnName, + name: aggDerivativeFnName, help: i18n.translate('data.search.aggs.function.metrics.derivative.help', { defaultMessage: 'Generates a serialized agg config for a Derivative agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/geo_bounds.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds.ts index c86f42f066bdf..5157ef1a134a7 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_bounds.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_bounds.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggGeoBoundsFnName } from './geo_bounds_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -38,6 +39,7 @@ const geoBoundsLabel = i18n.translate('data.search.aggs.metrics.geoBoundsLabel', export const getGeoBoundsMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.GEO_BOUNDS, + expressionName: aggGeoBoundsFnName, title: geoBoundsTitle, makeLabel: () => geoBoundsLabel, params: [ diff --git a/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts index 927f7f42d0f50..af5ea3c80506c 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggGeoBounds'; +export const aggGeoBoundsFnName = 'aggGeoBounds'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.GEO_BOUNDS>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggGeoBoundsFnName, + Input, + AggArgs, + Output +>; export const aggGeoBounds = (): FunctionDefinition => ({ - name: fnName, + name: aggGeoBoundsFnName, help: i18n.translate('data.search.aggs.function.metrics.geo_bounds.help', { defaultMessage: 'Generates a serialized agg config for a Geo Bounds agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/geo_centroid.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid.ts index b98ce45d35229..c293d4a4b1620 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_centroid.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_centroid.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggGeoCentroidFnName } from './geo_centroid_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -38,6 +39,7 @@ const geoCentroidLabel = i18n.translate('data.search.aggs.metrics.geoCentroidLab export const getGeoCentroidMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.GEO_CENTROID, + expressionName: aggGeoCentroidFnName, title: geoCentroidTitle, makeLabel: () => geoCentroidLabel, params: [ diff --git a/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts index 98bd7365f8b3f..2c2d60711def3 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggGeoCentroid'; +export const aggGeoCentroidFnName = 'aggGeoCentroid'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.GEO_CENTROID>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggGeoCentroidFnName, + Input, + AggArgs, + Output +>; export const aggGeoCentroid = (): FunctionDefinition => ({ - name: fnName, + name: aggGeoCentroidFnName, help: i18n.translate('data.search.aggs.function.metrics.geo_centroid.help', { defaultMessage: 'Generates a serialized agg config for a Geo Centroid agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/max.ts b/src/plugins/data/common/search/aggs/metrics/max.ts index 5b2f08c5b0260..f69b64c47f652 100644 --- a/src/plugins/data/common/search/aggs/metrics/max.ts +++ b/src/plugins/data/common/search/aggs/metrics/max.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggMaxFnName } from './max_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -34,6 +35,7 @@ export interface AggParamsMax extends BaseAggParams { export const getMaxMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.MAX, + expressionName: aggMaxFnName, title: maxTitle, makeLabel(aggConfig) { return i18n.translate('data.search.aggs.metrics.maxLabel', { diff --git a/src/plugins/data/common/search/aggs/metrics/max_fn.ts b/src/plugins/data/common/search/aggs/metrics/max_fn.ts index d1bccd08982f8..9624cd3012398 100644 --- a/src/plugins/data/common/search/aggs/metrics/max_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/max_fn.ts @@ -22,15 +22,15 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggMax'; +export const aggMaxFnName = 'aggMax'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.MAX>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition<typeof aggMaxFnName, Input, AggArgs, Output>; export const aggMax = (): FunctionDefinition => ({ - name: fnName, + name: aggMaxFnName, help: i18n.translate('data.search.aggs.function.metrics.max.help', { defaultMessage: 'Generates a serialized agg config for a Max agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts index 42298586cb68f..42ea942098c4a 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -82,4 +82,28 @@ describe('AggTypeMetricMedianProvider class', () => { }) ).toEqual(10); }); + + it('produces the expected expression ast', () => { + const agg = aggConfigs.getResponseAggs()[0]; + expect(agg.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "median", + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggMedian", + "type": "function", + } + `); + }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/median.ts b/src/plugins/data/common/search/aggs/metrics/median.ts index a189461020915..c511a7018575d 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { aggMedianFnName } from './median_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -34,6 +35,7 @@ export interface AggParamsMedian extends BaseAggParams { export const getMedianMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.MEDIAN, + expressionName: aggMedianFnName, dslName: 'percentiles', title: medianTitle, makeLabel(aggConfig) { diff --git a/src/plugins/data/common/search/aggs/metrics/median_fn.ts b/src/plugins/data/common/search/aggs/metrics/median_fn.ts index c5e9edb86e81c..e2ea8ae0fe2e7 100644 --- a/src/plugins/data/common/search/aggs/metrics/median_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/median_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggMedian'; +export const aggMedianFnName = 'aggMedian'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.MEDIAN>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggMedianFnName, + Input, + AggArgs, + Output +>; export const aggMedian = (): FunctionDefinition => ({ - name: fnName, + name: aggMedianFnName, help: i18n.translate('data.search.aggs.function.metrics.median.help', { defaultMessage: 'Generates a serialized agg config for a Median agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/min.ts b/src/plugins/data/common/search/aggs/metrics/min.ts index 6472c3ae12990..a0ed0cd19c127 100644 --- a/src/plugins/data/common/search/aggs/metrics/min.ts +++ b/src/plugins/data/common/search/aggs/metrics/min.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; +import { aggMinFnName } from './min_fn'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BaseAggParams } from '../types'; @@ -34,6 +35,7 @@ export interface AggParamsMin extends BaseAggParams { export const getMinMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.MIN, + expressionName: aggMinFnName, title: minTitle, makeLabel(aggConfig) { return i18n.translate('data.search.aggs.metrics.minLabel', { diff --git a/src/plugins/data/common/search/aggs/metrics/min_fn.ts b/src/plugins/data/common/search/aggs/metrics/min_fn.ts index 7a57c79a350fa..b880937eea2d7 100644 --- a/src/plugins/data/common/search/aggs/metrics/min_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/min_fn.ts @@ -22,15 +22,15 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggMin'; +export const aggMinFnName = 'aggMin'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.MIN>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition<typeof aggMinFnName, Input, AggArgs, Output>; export const aggMin = (): FunctionDefinition => ({ - name: fnName, + name: aggMinFnName, help: i18n.translate('data.search.aggs.function.metrics.min.help', { defaultMessage: 'Generates a serialized agg config for a Min agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/moving_avg.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg.ts index 1791d49b98437..60e0f4293cb9e 100644 --- a/src/plugins/data/common/search/aggs/metrics/moving_avg.ts +++ b/src/plugins/data/common/search/aggs/metrics/moving_avg.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; +import { aggMovingAvgFnName } from './moving_avg_fn'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; @@ -45,6 +46,7 @@ export const getMovingAvgMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.MOVING_FN, + expressionName: aggMovingAvgFnName, dslName: 'moving_fn', title: movingAvgTitle, makeLabel: (agg) => makeNestedLabel(agg, movingAvgLabel), diff --git a/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts index e1c1637d3ad1d..f517becf2bd65 100644 --- a/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts @@ -23,16 +23,21 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggMovingAvg'; +export const aggMovingAvgFnName = 'aggMovingAvg'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.MOVING_FN>; type Arguments = Assign<AggArgs, { customMetric?: AggExpressionType }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggMovingAvgFnName, + Input, + Arguments, + Output +>; export const aggMovingAvg = (): FunctionDefinition => ({ - name: fnName, + name: aggMovingAvgFnName, help: i18n.translate('data.search.aggs.function.metrics.moving_avg.help', { defaultMessage: 'Generates a serialized agg config for a Moving Average agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts index 970daf5b62458..9955aeef4e0d2 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts @@ -63,7 +63,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function () { ); }); - it('uses the custom label if it is set', function () { + it('uses the custom label if it is set', () => { const responseAggs: any = getPercentileRanksMetricAgg(aggTypesDependencies).getResponseAggs( aggConfigs.aggs[0] as IPercentileRanksAggConfig ); @@ -74,4 +74,62 @@ describe('AggTypesMetricsPercentileRanksProvider class', function () { expect(percentileRankLabelFor5kBytes).toBe('Percentile rank 5000 of "my custom field label"'); expect(percentileRankLabelFor10kBytes).toBe('Percentile rank 10000 of "my custom field label"'); }); + + it('produces the expected expression ast', () => { + const responseAggs: any = getPercentileRanksMetricAgg(aggTypesDependencies).getResponseAggs( + aggConfigs.aggs[0] as IPercentileRanksAggConfig + ); + expect(responseAggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "customLabel": Array [ + "my custom field label", + ], + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "percentile_ranks.5000", + ], + "schema": Array [ + "metric", + ], + "values": Array [ + "[5000,10000]", + ], + }, + "function": "aggPercentileRanks", + "type": "function", + } + `); + expect(responseAggs[1].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "customLabel": Array [ + "my custom field label", + ], + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "percentile_ranks.10000", + ], + "schema": Array [ + "metric", + ], + "values": Array [ + "[5000,10000]", + ], + }, + "function": "aggPercentileRanks", + "type": "function", + } + `); + }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts index 664cc1ad02ada..5260f52731a88 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts @@ -25,6 +25,7 @@ import { BaseAggParams } from '../types'; import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; +import { aggPercentileRanksFnName } from './percentile_ranks_fn'; import { getPercentileValue } from './percentiles_get_value'; import { METRIC_TYPES } from './metric_agg_types'; @@ -64,6 +65,7 @@ export const getPercentileRanksMetricAgg = ({ }: PercentileRanksMetricAggDependencies) => { return new MetricAggType<IPercentileRanksAggConfig>({ name: METRIC_TYPES.PERCENTILE_RANKS, + expressionName: aggPercentileRanksFnName, title: i18n.translate('data.search.aggs.metrics.percentileRanksTitle', { defaultMessage: 'Percentile Ranks', }), diff --git a/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts index 08e1489a856dd..9bf35c4dba9ff 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggPercentileRanks'; +export const aggPercentileRanksFnName = 'aggPercentileRanks'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.PERCENTILE_RANKS>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggPercentileRanksFnName, + Input, + AggArgs, + Output +>; export const aggPercentileRanks = (): FunctionDefinition => ({ - name: fnName, + name: aggPercentileRanksFnName, help: i18n.translate('data.search.aggs.function.metrics.percentile_ranks.help', { defaultMessage: 'Generates a serialized agg config for a Percentile Ranks agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts index 10e98df5a4eeb..78b00a48a9611 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts @@ -66,4 +66,36 @@ describe('AggTypesMetricsPercentilesProvider class', () => { expect(ninetyFifthPercentileLabel).toBe('95th percentile of prince'); }); + + it('produces the expected expression ast', () => { + const responseAggs: any = getPercentilesMetricAgg().getResponseAggs( + aggConfigs.aggs[0] as IPercentileAggConfig + ); + expect(responseAggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "customLabel": Array [ + "prince", + ], + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "percentiles.95", + ], + "percents": Array [ + "[95]", + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggPercentiles", + "type": "function", + } + `); + }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/percentiles.ts b/src/plugins/data/common/search/aggs/metrics/percentiles.ts index 8ea493f324811..22aeb820dbe0b 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentiles.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles.ts @@ -22,6 +22,7 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; +import { aggPercentilesFnName } from './percentiles_fn'; import { getPercentileValue } from './percentiles_get_value'; import { ordinalSuffix } from './lib/ordinal_suffix'; import { BaseAggParams } from '../types'; @@ -48,6 +49,7 @@ const valueProps = { export const getPercentilesMetricAgg = () => { return new MetricAggType<IPercentileAggConfig>({ name: METRIC_TYPES.PERCENTILES, + expressionName: aggPercentilesFnName, title: i18n.translate('data.search.aggs.metrics.percentilesTitle', { defaultMessage: 'Percentiles', }), diff --git a/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts index eb8952267f5ea..d7bcefc23f711 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggPercentiles'; +export const aggPercentilesFnName = 'aggPercentiles'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.PERCENTILES>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggPercentilesFnName, + Input, + AggArgs, + Output +>; export const aggPercentiles = (): FunctionDefinition => ({ - name: fnName, + name: aggPercentilesFnName, help: i18n.translate('data.search.aggs.function.metrics.percentiles.help', { defaultMessage: 'Generates a serialized agg config for a Percentiles agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/serial_diff.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff.ts index a4e4d7a8990fa..30158a312289f 100644 --- a/src/plugins/data/common/search/aggs/metrics/serial_diff.ts +++ b/src/plugins/data/common/search/aggs/metrics/serial_diff.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; +import { aggSerialDiffFnName } from './serial_diff_fn'; import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; @@ -43,6 +44,7 @@ export const getSerialDiffMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.SERIAL_DIFF, + expressionName: aggSerialDiffFnName, title: serialDiffTitle, makeLabel: (agg) => makeNestedLabel(agg, serialDiffLabel), subtype, diff --git a/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts index 3cc1dacb87b3d..96f82e430a0b4 100644 --- a/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts @@ -23,16 +23,21 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggSerialDiff'; +export const aggSerialDiffFnName = 'aggSerialDiff'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.SERIAL_DIFF>; type Arguments = Assign<AggArgs, { customMetric?: AggExpressionType }>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggSerialDiffFnName, + Input, + Arguments, + Output +>; export const aggSerialDiff = (): FunctionDefinition => ({ - name: fnName, + name: aggSerialDiffFnName, help: i18n.translate('data.search.aggs.function.metrics.serial_diff.help', { defaultMessage: 'Generates a serialized agg config for a Serial Differencing agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts index f2f30fcde42eb..6ca0c6698376f 100644 --- a/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts @@ -82,4 +82,29 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { expect(lowerStdDevLabel).toBe('Lower Standard Deviation of memory'); expect(upperStdDevLabel).toBe('Upper Standard Deviation of memory'); }); + + it('produces the expected expression ast', () => { + const aggConfigs = getAggConfigs(); + + const responseAggs: any = getStdDeviationMetricAgg().getResponseAggs( + aggConfigs.aggs[0] as IStdDevAggConfig + ); + expect(responseAggs[0].toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "std_dev.std_lower", + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggStdDeviation", + "type": "function", + } + `); + }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/std_deviation.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation.ts index 9aba063776252..88b2fd69e2b85 100644 --- a/src/plugins/data/common/search/aggs/metrics/std_deviation.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation.ts @@ -20,6 +20,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; +import { aggStdDeviationFnName } from './std_deviation_fn'; import { METRIC_TYPES } from './metric_agg_types'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -83,6 +84,7 @@ const responseAggConfigProps = { export const getStdDeviationMetricAgg = () => { return new MetricAggType<IStdDevAggConfig>({ name: METRIC_TYPES.STD_DEV, + expressionName: aggStdDeviationFnName, dslName: 'extended_stats', title: i18n.translate('data.search.aggs.metrics.standardDeviationTitle', { defaultMessage: 'Standard Deviation', diff --git a/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts index 61b8a6f28f088..2a3c1bd33e17d 100644 --- a/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggStdDeviation'; +export const aggStdDeviationFnName = 'aggStdDeviation'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.STD_DEV>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggStdDeviationFnName, + Input, + AggArgs, + Output +>; export const aggStdDeviation = (): FunctionDefinition => ({ - name: fnName, + name: aggStdDeviationFnName, help: i18n.translate('data.search.aggs.function.metrics.std_deviation.help', { defaultMessage: 'Generates a serialized agg config for a Standard Deviation agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/sum.ts b/src/plugins/data/common/search/aggs/metrics/sum.ts index fa44af98554da..c24887b5e0818 100644 --- a/src/plugins/data/common/search/aggs/metrics/sum.ts +++ b/src/plugins/data/common/search/aggs/metrics/sum.ts @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; +import { aggSumFnName } from './sum_fn'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BaseAggParams } from '../types'; @@ -34,6 +35,7 @@ export interface AggParamsSum extends BaseAggParams { export const getSumMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.SUM, + expressionName: aggSumFnName, title: sumTitle, makeLabel(aggConfig) { return i18n.translate('data.search.aggs.metrics.sumLabel', { diff --git a/src/plugins/data/common/search/aggs/metrics/sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts index e625befc8f1d9..a42510dc594ad 100644 --- a/src/plugins/data/common/search/aggs/metrics/sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts @@ -22,15 +22,15 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggSum'; +export const aggSumFnName = 'aggSum'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.SUM>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition<typeof aggSumFnName, Input, AggArgs, Output>; export const aggSum = (): FunctionDefinition => ({ - name: fnName, + name: aggSumFnName, help: i18n.translate('data.search.aggs.function.metrics.sum.help', { defaultMessage: 'Generates a serialized agg config for a Sum agg', }), diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts index c0cbfb33c842b..2fdefa7679e9b 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts @@ -102,6 +102,42 @@ describe('Top hit metric', () => { expect(getTopHitMetricAgg().makeLabel(aggConfig)).toEqual('First bytes'); }); + it('produces the expected expression ast', () => { + init({ fieldName: 'machine.os' }); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "aggregate": Array [ + "concat", + ], + "enabled": Array [ + true, + ], + "field": Array [ + "machine.os", + ], + "id": Array [ + "1", + ], + "schema": Array [ + "metric", + ], + "size": Array [ + 1, + ], + "sortField": Array [ + "machine.os", + ], + "sortOrder": Array [ + "desc", + ], + }, + "function": "aggTopHit", + "type": "function", + } + `); + }); + it('should request the _source field', () => { init({ field: '_source' }); expect(aggDsl.top_hits._source).toBeTruthy(); diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.ts index bee731dcc2e0d..3ef9f9ffa3ad0 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.ts @@ -19,6 +19,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { aggTopHitFnName } from './top_hit_fn'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; @@ -41,6 +42,7 @@ const isNumericFieldSelected = (agg: IMetricAggConfig) => { export const getTopHitMetricAgg = () => { return new MetricAggType({ name: METRIC_TYPES.TOP_HITS, + expressionName: aggTopHitFnName, title: i18n.translate('data.search.aggs.metrics.topHitTitle', { defaultMessage: 'Top Hit', }), diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts index e0c3fd0d070b2..38a3bc6a59bfc 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts @@ -22,15 +22,20 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; -const fnName = 'aggTopHit'; +export const aggTopHitFnName = 'aggTopHit'; type Input = any; type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.TOP_HITS>; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTopHitFnName, + Input, + AggArgs, + Output +>; export const aggTopHit = (): FunctionDefinition => ({ - name: fnName, + name: aggTopHitFnName, help: i18n.translate('data.search.aggs.function.metrics.top_hit.help', { defaultMessage: 'Generates a serialized agg config for a Top Hit agg', }), diff --git a/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts b/src/plugins/data/common/search/expressions/esaggs/build_tabular_inspector_data.ts similarity index 95% rename from src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts rename to src/plugins/data/common/search/expressions/esaggs/build_tabular_inspector_data.ts index 79dedf4131764..2db3694884e2c 100644 --- a/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts +++ b/src/plugins/data/common/search/expressions/esaggs/build_tabular_inspector_data.ts @@ -23,9 +23,10 @@ import { TabularData, TabularDataValue, } from '../../../../../../plugins/inspector/common'; -import { Filter, TabbedTable } from '../../../../common'; -import { FormatFactory } from '../../../../common/field_formats/utils'; -import { createFilter } from '../create_filter'; +import { Filter } from '../../../es_query'; +import { FormatFactory } from '../../../field_formats/utils'; +import { TabbedTable } from '../../tabify'; +import { createFilter } from './create_filter'; /** * Type borrowed from the client-side FilterManager['addFilters']. diff --git a/src/plugins/data/public/search/expressions/create_filter.test.ts b/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts similarity index 91% rename from src/plugins/data/public/search/expressions/create_filter.test.ts rename to src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts index 7cc336a1c20e9..de0990ea9e287 100644 --- a/src/plugins/data/public/search/expressions/create_filter.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts @@ -17,15 +17,11 @@ * under the License. */ -import { - AggConfigs, - IAggConfig, - TabbedTable, - isRangeFilter, - BytesFormat, - FieldFormatsGetConfigFn, -} from '../../../common'; -import { mockAggTypesRegistry } from '../../../common/search/aggs/test_helpers'; +import { isRangeFilter } from '../../../es_query/filters'; +import { BytesFormat, FieldFormatsGetConfigFn } from '../../../field_formats'; +import { AggConfigs, IAggConfig } from '../../aggs'; +import { mockAggTypesRegistry } from '../../aggs/test_helpers'; +import { TabbedTable } from '../../tabify'; import { createFilter } from './create_filter'; diff --git a/src/plugins/data/public/search/expressions/create_filter.ts b/src/plugins/data/common/search/expressions/esaggs/create_filter.ts similarity index 94% rename from src/plugins/data/public/search/expressions/create_filter.ts rename to src/plugins/data/common/search/expressions/esaggs/create_filter.ts index 09200c2e17b31..cfb406e18e6c3 100644 --- a/src/plugins/data/public/search/expressions/create_filter.ts +++ b/src/plugins/data/common/search/expressions/esaggs/create_filter.ts @@ -17,7 +17,9 @@ * under the License. */ -import { Filter, IAggConfig, TabbedTable } from '../../../common'; +import { Filter } from '../../../es_query'; +import { IAggConfig } from '../../aggs'; +import { TabbedTable } from '../../tabify'; const getOtherBucketFilterTerms = (table: TabbedTable, columnIndex: number, rowIndex: number) => { if (rowIndex === -1) { diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts new file mode 100644 index 0000000000000..ca1234276f416 --- /dev/null +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -0,0 +1,154 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +import { + Datatable, + DatatableColumn, + ExpressionFunctionDefinition, +} from 'src/plugins/expressions/common'; + +import { FormatFactory } from '../../../field_formats/utils'; +import { IndexPatternsContract } from '../../../index_patterns/index_patterns'; +import { calculateBounds } from '../../../query'; + +import { AggsStart } from '../../aggs'; +import { ISearchStartSearchSource } from '../../search_source'; + +import { KibanaContext } from '../kibana_context_type'; +import { AddFilters } from './build_tabular_inspector_data'; +import { handleRequest, RequestHandlerParams } from './request_handler'; + +const name = 'esaggs'; + +type Input = KibanaContext | null; +type Output = Promise<Datatable>; + +interface Arguments { + index: string; + metricsAtAllLevels: boolean; + partialRows: boolean; + includeFormatHints: boolean; + aggConfigs: string; + timeFields?: string[]; +} + +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'esaggs', + Input, + Arguments, + Output +>; + +/** @internal */ +export interface EsaggsStartDependencies { + addFilters?: AddFilters; + aggs: AggsStart; + deserializeFieldFormat: FormatFactory; + indexPatterns: IndexPatternsContract; + searchSource: ISearchStartSearchSource; +} + +/** @internal */ +export const getEsaggsMeta: () => Omit<EsaggsExpressionFunctionDefinition, 'fn'> = () => ({ + name, + type: 'datatable', + inputTypes: ['kibana_context', 'null'], + help: i18n.translate('data.functions.esaggs.help', { + defaultMessage: 'Run AggConfig aggregation', + }), + args: { + index: { + types: ['string'], + help: '', + }, + metricsAtAllLevels: { + types: ['boolean'], + default: false, + help: '', + }, + partialRows: { + types: ['boolean'], + default: false, + help: '', + }, + includeFormatHints: { + types: ['boolean'], + default: false, + help: '', + }, + aggConfigs: { + types: ['string'], + default: '""', + help: '', + }, + timeFields: { + types: ['string'], + help: '', + multi: true, + }, + }, +}); + +/** @internal */ +export async function handleEsaggsRequest( + input: Input, + args: Arguments, + params: RequestHandlerParams +): Promise<Datatable> { + const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); + + const response = await handleRequest(params); + + const table: Datatable = { + type: 'datatable', + rows: response.rows, + columns: response.columns.map((column) => { + const cleanedColumn: DatatableColumn = { + id: column.id, + name: column.name, + meta: { + type: column.aggConfig.params.field?.type || 'number', + field: column.aggConfig.params.field?.name, + index: params.indexPattern?.title, + params: column.aggConfig.toSerializedFieldFormat(), + source: name, + sourceParams: { + indexPatternId: params.indexPattern?.id, + appliedTimeRange: + column.aggConfig.params.field?.name && + input?.timeRange && + args.timeFields && + args.timeFields.includes(column.aggConfig.params.field?.name) + ? { + from: resolvedTimeRange?.min?.toISOString(), + to: resolvedTimeRange?.max?.toISOString(), + } + : undefined, + ...column.aggConfig.serialize(), + }, + }, + }; + return cleanedColumn; + }), + }; + + return table; +} diff --git a/src/plugins/data/public/search/expressions/esaggs/index.ts b/src/plugins/data/common/search/expressions/esaggs/index.ts similarity index 100% rename from src/plugins/data/public/search/expressions/esaggs/index.ts rename to src/plugins/data/common/search/expressions/esaggs/index.ts diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts similarity index 99% rename from src/plugins/data/public/search/expressions/esaggs/request_handler.ts rename to src/plugins/data/common/search/expressions/esaggs/request_handler.ts index 7a27d65267149..a424ed9e0513d 100644 --- a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -40,7 +40,8 @@ import { FormatFactory } from '../../../../common/field_formats/utils'; import { AddFilters, buildTabularInspectorData } from './build_tabular_inspector_data'; -interface RequestHandlerParams { +/** @internal */ +export interface RequestHandlerParams { abortSignal?: AbortSignal; addFilters?: AddFilters; aggs: IAggConfigs; diff --git a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts index 378ceb376f5f1..eebe1ab80a536 100644 --- a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts +++ b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts @@ -17,22 +17,27 @@ * under the License. */ -import { indexPatternLoad } from './load_index_pattern'; - -jest.mock('../../services', () => ({ - getIndexPatterns: () => ({ - get: (id: string) => ({ - toSpec: () => ({ - title: 'value', - }), - }), - }), -})); +import { IndexPatternLoadStartDependencies } from '../../../common/index_patterns/expressions'; +import { getFunctionDefinition } from './load_index_pattern'; describe('indexPattern expression function', () => { + let getStartDependencies: () => Promise<IndexPatternLoadStartDependencies>; + + beforeEach(() => { + getStartDependencies = jest.fn().mockResolvedValue({ + indexPatterns: { + get: (id: string) => ({ + toSpec: () => ({ + title: 'value', + }), + }), + }, + }); + }); + test('returns serialized index pattern', async () => { - const indexPatternDefinition = indexPatternLoad(); - const result = await indexPatternDefinition.fn(null, { id: '1' }, {} as any); + const indexPatternDefinition = getFunctionDefinition({ getStartDependencies }); + const result = await indexPatternDefinition().fn(null, { id: '1' }, {} as any); expect(result.type).toEqual('index_pattern'); expect(result.value.title).toEqual('value'); }); diff --git a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts index 901d6aac7fbff..64e86f967c2b1 100644 --- a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts +++ b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts @@ -17,46 +17,66 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../plugins/expressions/public'; -import { getIndexPatterns } from '../../services'; -import { IndexPatternSpec } from '../../../common/index_patterns'; +import { StartServicesAccessor } from 'src/core/public'; +import { + getIndexPatternLoadMeta, + IndexPatternLoadExpressionFunctionDefinition, + IndexPatternLoadStartDependencies, +} from '../../../common/index_patterns/expressions'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; -const name = 'indexPatternLoad'; +/** + * Returns the expression function definition. Any stateful dependencies are accessed + * at runtime via the `getStartDependencies` param, which provides the specific services + * needed for this function to run. + * + * This function is an implementation detail of this module, and is exported separately + * only for testing purposes. + * + * @param getStartDependencies - async function that resolves with IndexPatternLoadStartDependencies + * + * @internal + */ +export function getFunctionDefinition({ + getStartDependencies, +}: { + getStartDependencies: () => Promise<IndexPatternLoadStartDependencies>; +}) { + return (): IndexPatternLoadExpressionFunctionDefinition => ({ + ...getIndexPatternLoadMeta(), + async fn(input, args) { + const { indexPatterns } = await getStartDependencies(); -type Input = null; -type Output = Promise<{ type: 'index_pattern'; value: IndexPatternSpec }>; + const indexPattern = await indexPatterns.get(args.id); -interface Arguments { - id: string; + return { type: 'index_pattern', value: indexPattern.toSpec() }; + }, + }); } -export const indexPatternLoad = (): ExpressionFunctionDefinition< - typeof name, - Input, - Arguments, - Output -> => ({ - name, - type: 'index_pattern', - inputTypes: ['null'], - help: i18n.translate('data.functions.indexPatternLoad.help', { - defaultMessage: 'Loads an index pattern', - }), - args: { - id: { - types: ['string'], - required: true, - help: i18n.translate('data.functions.indexPatternLoad.id.help', { - defaultMessage: 'index pattern id to load', - }), +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getIndexPatternLoad({ + getStartServices, +}: { + getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>; +}) { + return getFunctionDefinition({ + getStartDependencies: async () => { + const [, , { indexPatterns }] = await getStartServices(); + return { indexPatterns }; }, - }, - async fn(input, args) { - const indexPatterns = getIndexPatterns(); - - const indexPattern = await indexPatterns.get(args.id); - - return { type: 'index_pattern', value: indexPattern.toSpec() }; - }, -}); + }); +} diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 8d40447a48ff0..3c8ea0351dee6 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -48,7 +48,6 @@ import { setUiSettings, } from './services'; import { createSearchBar } from './ui/search_bar/create_search_bar'; -import { getEsaggs } from './search/expressions'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, @@ -69,7 +68,7 @@ import { } from './actions'; import { SavedObjectsClientPublicToCommon } from './index_patterns'; -import { indexPatternLoad } from './index_patterns/expressions/load_index_pattern'; +import { getIndexPatternLoad } from './index_patterns/expressions'; import { UsageCollectionSetup } from '../../usage_collection/public'; declare module '../../ui_actions/public' { @@ -109,22 +108,7 @@ export class DataPublicPlugin ): DataPublicPluginSetup { const startServices = createStartServicesGetter(core.getStartServices); - expressions.registerFunction(indexPatternLoad); - expressions.registerFunction( - getEsaggs({ - getStartDependencies: async () => { - const [, , self] = await core.getStartServices(); - const { fieldFormats, indexPatterns, query, search } = self; - return { - addFilters: query.filterManager.addFilters.bind(query.filterManager), - aggs: search.aggs, - deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats), - indexPatterns, - searchSource: search.searchSource, - }; - }, - }) - ); + expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); this.usageCollection = usageCollection; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 484f07633e203..5201cd3c211e9 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2163,7 +2163,7 @@ export class SearchSource { type?: string | undefined; query?: import("../..").Query | undefined; filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric> | Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric>[] | undefined; + sort?: Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection> | Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection>[] | undefined; highlight?: any; highlightAll?: boolean | undefined; aggs?: any; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts new file mode 100644 index 0000000000000..efb31423afcdf --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { StartServicesAccessor } from 'src/core/public'; +import { Adapters } from 'src/plugins/inspector/common'; +import { + EsaggsExpressionFunctionDefinition, + EsaggsStartDependencies, + getEsaggsMeta, + handleEsaggsRequest, +} from '../../../common/search/expressions'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; + +/** + * Returns the expression function definition. Any stateful dependencies are accessed + * at runtime via the `getStartDependencies` param, which provides the specific services + * needed for this function to run. + * + * This function is an implementation detail of this module, and is exported separately + * only for testing purposes. + * + * @param getStartDependencies - async function that resolves with EsaggsStartDependencies + * + * @internal + */ +export function getFunctionDefinition({ + getStartDependencies, +}: { + getStartDependencies: () => Promise<EsaggsStartDependencies>; +}) { + return (): EsaggsExpressionFunctionDefinition => ({ + ...getEsaggsMeta(), + async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { + const { + addFilters, + aggs, + deserializeFieldFormat, + indexPatterns, + searchSource, + } = await getStartDependencies(); + + const aggConfigsState = JSON.parse(args.aggConfigs); + const indexPattern = await indexPatterns.get(args.index); + const aggConfigs = aggs.createAggConfigs(indexPattern, aggConfigsState); + + return await handleEsaggsRequest(input, args, { + abortSignal: (abortSignal as unknown) as AbortSignal, + addFilters, + aggs: aggConfigs, + deserializeFieldFormat, + filters: get(input, 'filters', undefined), + indexPattern, + inspectorAdapters: inspectorAdapters as Adapters, + metricsAtAllLevels: args.metricsAtAllLevels, + partialRows: args.partialRows, + query: get(input, 'query', undefined) as any, + searchSessionId: getSearchSessionId(), + searchSourceService: searchSource, + timeFields: args.timeFields, + timeRange: get(input, 'timeRange', undefined), + }); + }, + }); +} + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getEsaggs({ + getStartServices, +}: { + getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>; +}) { + return getFunctionDefinition({ + getStartDependencies: async () => { + const [, , self] = await getStartServices(); + const { fieldFormats, indexPatterns, query, search } = self; + return { + addFilters: query.filterManager.addFilters.bind(query.filterManager), + aggs: search.aggs, + deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats), + indexPatterns, + searchSource: search.searchSource, + }; + }, + }); +} diff --git a/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts deleted file mode 100644 index ce3bd9bdaee76..0000000000000 --- a/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; - -import { Datatable, DatatableColumn } from 'src/plugins/expressions/common'; -import { Adapters } from 'src/plugins/inspector/common'; - -import { calculateBounds, EsaggsExpressionFunctionDefinition } from '../../../../common'; -import { FormatFactory } from '../../../../common/field_formats/utils'; -import { IndexPatternsContract } from '../../../../common/index_patterns/index_patterns'; -import { ISearchStartSearchSource, AggsStart } from '../../../../common/search'; - -import { AddFilters } from './build_tabular_inspector_data'; -import { handleRequest } from './request_handler'; - -const name = 'esaggs'; - -interface StartDependencies { - addFilters: AddFilters; - aggs: AggsStart; - deserializeFieldFormat: FormatFactory; - indexPatterns: IndexPatternsContract; - searchSource: ISearchStartSearchSource; -} - -export function getEsaggs({ - getStartDependencies, -}: { - getStartDependencies: () => Promise<StartDependencies>; -}) { - return (): EsaggsExpressionFunctionDefinition => ({ - name, - type: 'datatable', - inputTypes: ['kibana_context', 'null'], - help: i18n.translate('data.functions.esaggs.help', { - defaultMessage: 'Run AggConfig aggregation', - }), - args: { - index: { - types: ['string'], - help: '', - }, - metricsAtAllLevels: { - types: ['boolean'], - default: false, - help: '', - }, - partialRows: { - types: ['boolean'], - default: false, - help: '', - }, - includeFormatHints: { - types: ['boolean'], - default: false, - help: '', - }, - aggConfigs: { - types: ['string'], - default: '""', - help: '', - }, - timeFields: { - types: ['string'], - help: '', - multi: true, - }, - }, - async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { - const { - addFilters, - aggs, - deserializeFieldFormat, - indexPatterns, - searchSource, - } = await getStartDependencies(); - - const aggConfigsState = JSON.parse(args.aggConfigs); - const indexPattern = await indexPatterns.get(args.index); - const aggConfigs = aggs.createAggConfigs(indexPattern, aggConfigsState); - - const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); - - const response = await handleRequest({ - abortSignal: (abortSignal as unknown) as AbortSignal, - addFilters, - aggs: aggConfigs, - deserializeFieldFormat, - filters: get(input, 'filters', undefined), - indexPattern, - inspectorAdapters: inspectorAdapters as Adapters, - metricsAtAllLevels: args.metricsAtAllLevels, - partialRows: args.partialRows, - query: get(input, 'query', undefined) as any, - searchSessionId: getSearchSessionId(), - searchSourceService: searchSource, - timeFields: args.timeFields, - timeRange: get(input, 'timeRange', undefined), - }); - - const table: Datatable = { - type: 'datatable', - rows: response.rows, - columns: response.columns.map((column) => { - const cleanedColumn: DatatableColumn = { - id: column.id, - name: column.name, - meta: { - type: column.aggConfig.params.field?.type || 'number', - field: column.aggConfig.params.field?.name, - index: indexPattern.title, - params: column.aggConfig.toSerializedFieldFormat(), - source: name, - sourceParams: { - indexPatternId: indexPattern.id, - appliedTimeRange: - column.aggConfig.params.field?.name && - input?.timeRange && - args.timeFields && - args.timeFields.includes(column.aggConfig.params.field?.name) - ? { - from: resolvedTimeRange?.min?.toISOString(), - to: resolvedTimeRange?.max?.toISOString(), - } - : undefined, - ...column.aggConfig.serialize(), - }, - }, - }; - return cleanedColumn; - }), - }; - - return table; - }, - }); -} diff --git a/src/plugins/data/public/search/expressions/index.ts b/src/plugins/data/public/search/expressions/index.ts index 98ed1d08af8ad..9482a9748c466 100644 --- a/src/plugins/data/public/search/expressions/index.ts +++ b/src/plugins/data/public/search/expressions/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export * from './esaggs'; export * from './es_raw_response'; +export * from './esaggs'; export * from './esdsl'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 60d2dfdf866cf..1c49de8f0ff4b 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -17,7 +17,13 @@ * under the License. */ -import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { + Plugin, + CoreSetup, + CoreStart, + PluginInitializerContext, + StartServicesAccessor, +} from 'src/core/public'; import { BehaviorSubject } from 'rxjs'; import { BfetchPublicSetup } from 'src/plugins/bfetch/public'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; @@ -37,7 +43,7 @@ import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -import { esdsl, esRawResponse } from './expressions'; +import { esdsl, esRawResponse, getEsaggs } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session'; import { ConfigSchema } from '../../config'; @@ -46,6 +52,7 @@ import { getShardDelayBucketAgg, } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; +import { DataPublicPluginStart, DataStartDependencies } from '../types'; /** @internal */ export interface SearchServiceSetupDependencies { @@ -96,6 +103,11 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { session: this.sessionService, }); + expressions.registerFunction( + getEsaggs({ getStartServices } as { + getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>; + }) + ); expressions.registerFunction(kibana); expressions.registerFunction(kibanaContextFunction); expressions.registerType(kibanaContext); diff --git a/src/plugins/data/server/index_patterns/expressions/index.ts b/src/plugins/data/server/index_patterns/expressions/index.ts new file mode 100644 index 0000000000000..fa37e3b216ac9 --- /dev/null +++ b/src/plugins/data/server/index_patterns/expressions/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './load_index_pattern'; diff --git a/src/plugins/data/server/index_patterns/expressions/load_index_pattern.test.ts b/src/plugins/data/server/index_patterns/expressions/load_index_pattern.test.ts new file mode 100644 index 0000000000000..944bd06d64891 --- /dev/null +++ b/src/plugins/data/server/index_patterns/expressions/load_index_pattern.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPatternLoadStartDependencies } from '../../../common/index_patterns/expressions'; +import { getFunctionDefinition } from './load_index_pattern'; + +describe('indexPattern expression function', () => { + let getStartDependencies: () => Promise<IndexPatternLoadStartDependencies>; + + beforeEach(() => { + getStartDependencies = jest.fn().mockResolvedValue({ + indexPatterns: { + get: (id: string) => ({ + toSpec: () => ({ + title: 'value', + }), + }), + }, + }); + }); + + test('returns serialized index pattern', async () => { + const indexPatternDefinition = getFunctionDefinition({ getStartDependencies }); + const result = await indexPatternDefinition().fn(null, { id: '1' }, { + getKibanaRequest: () => ({}), + } as any); + expect(result.type).toEqual('index_pattern'); + expect(result.value.title).toEqual('value'); + }); + + test('throws if getKibanaRequest is not available', async () => { + const indexPatternDefinition = getFunctionDefinition({ getStartDependencies }); + expect(async () => { + await indexPatternDefinition().fn(null, { id: '1' }, {} as any); + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"A KibanaRequest is required to execute this search on the server. Please provide a request object to the expression execution params."` + ); + }); +}); diff --git a/src/plugins/data/server/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/server/index_patterns/expressions/load_index_pattern.ts new file mode 100644 index 0000000000000..8cf8492f77a3f --- /dev/null +++ b/src/plugins/data/server/index_patterns/expressions/load_index_pattern.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { KibanaRequest, StartServicesAccessor } from 'src/core/server'; + +import { + getIndexPatternLoadMeta, + IndexPatternLoadExpressionFunctionDefinition, + IndexPatternLoadStartDependencies, +} from '../../../common/index_patterns/expressions'; +import { DataPluginStartDependencies, DataPluginStart } from '../../plugin'; + +/** + * Returns the expression function definition. Any stateful dependencies are accessed + * at runtime via the `getStartDependencies` param, which provides the specific services + * needed for this function to run. + * + * This function is an implementation detail of this module, and is exported separately + * only for testing purposes. + * + * @param getStartDependencies - async function that resolves with IndexPatternLoadStartDependencies + * + * @internal + */ +export function getFunctionDefinition({ + getStartDependencies, +}: { + getStartDependencies: (req: KibanaRequest) => Promise<IndexPatternLoadStartDependencies>; +}) { + return (): IndexPatternLoadExpressionFunctionDefinition => ({ + ...getIndexPatternLoadMeta(), + async fn(input, args, { getKibanaRequest }) { + const kibanaRequest = getKibanaRequest ? getKibanaRequest() : null; + if (!kibanaRequest) { + throw new Error( + i18n.translate('data.indexPatterns.indexPatternLoad.error.kibanaRequest', { + defaultMessage: + 'A KibanaRequest is required to execute this search on the server. ' + + 'Please provide a request object to the expression execution params.', + }) + ); + } + + const { indexPatterns } = await getStartDependencies(kibanaRequest); + + const indexPattern = await indexPatterns.get(args.id); + + return { type: 'index_pattern', value: indexPattern.toSpec() }; + }, + }); +} + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getIndexPatternLoad({ + getStartServices, +}: { + getStartServices: StartServicesAccessor<DataPluginStartDependencies, DataPluginStart>; +}) { + return getFunctionDefinition({ + getStartDependencies: async (request: KibanaRequest) => { + const [{ elasticsearch, savedObjects }, , { indexPatterns }] = await getStartServices(); + return { + indexPatterns: await indexPatterns.indexPatternsServiceFactory( + savedObjects.getScopedClient(request), + elasticsearch.client.asScoped(request).asCurrentUser + ), + }; + }, + }); +} diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index af2d4d6a73e0f..82c96ba4ff7dc 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -25,11 +25,14 @@ import { SavedObjectsClientContract, ElasticsearchClient, } from 'kibana/server'; +import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { IndexPatternsService as IndexPatternsCommonService } from '../../common/index_patterns'; import { FieldFormatsStart } from '../field_formats'; +import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; import { IndexPatternsApiServer } from './index_patterns_api_client'; import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; @@ -41,17 +44,26 @@ export interface IndexPatternsServiceStart { ) => Promise<IndexPatternsCommonService>; } +export interface IndexPatternsServiceSetupDeps { + expressions: ExpressionsServerSetup; +} + export interface IndexPatternsServiceStartDeps { fieldFormats: FieldFormatsStart; logger: Logger; } export class IndexPatternsService implements Plugin<void, IndexPatternsServiceStart> { - public setup(core: CoreSetup) { + public setup( + core: CoreSetup<DataPluginStartDependencies, DataPluginStart>, + { expressions }: IndexPatternsServiceSetupDeps + ) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); registerRoutes(core.http); + + expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); } public start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps) { diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index bba2c368ff7d1..12ad0dec0ccd1 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'src/core/server'; +import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ConfigSchema } from '../config'; @@ -89,7 +89,7 @@ export class DataServerPlugin core: CoreSetup<DataPluginStartDependencies, DataPluginStart>, { bfetch, expressions, usageCollection }: DataPluginSetupDependencies ) { - this.indexPatterns.setup(core); + this.indexPatterns.setup(core, { expressions }); this.scriptsService.setup(core); this.queryService.setup(core); this.autocompleteService.setup(core); diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts new file mode 100644 index 0000000000000..04cfcd1eef043 --- /dev/null +++ b/src/plugins/data/server/search/expressions/esaggs.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { KibanaRequest, StartServicesAccessor } from 'src/core/server'; +import { Adapters } from 'src/plugins/inspector/common'; +import { + EsaggsExpressionFunctionDefinition, + EsaggsStartDependencies, + getEsaggsMeta, + handleEsaggsRequest, +} from '../../../common/search/expressions'; +import { DataPluginStartDependencies, DataPluginStart } from '../../plugin'; + +/** + * Returns the expression function definition. Any stateful dependencies are accessed + * at runtime via the `getStartDependencies` param, which provides the specific services + * needed for this function to run. + * + * This function is an implementation detail of this module, and is exported separately + * only for testing purposes. + * + * @param getStartDependencies - async function that resolves with EsaggsStartDependencies + * + * @internal + */ +export function getFunctionDefinition({ + getStartDependencies, +}: { + getStartDependencies: (req: KibanaRequest) => Promise<EsaggsStartDependencies>; +}): () => EsaggsExpressionFunctionDefinition { + return () => ({ + ...getEsaggsMeta(), + async fn( + input, + args, + { inspectorAdapters, abortSignal, getSearchSessionId, getKibanaRequest } + ) { + const kibanaRequest = getKibanaRequest ? getKibanaRequest() : null; + if (!kibanaRequest) { + throw new Error( + i18n.translate('data.search.esaggs.error.kibanaRequest', { + defaultMessage: + 'A KibanaRequest is required to execute this search on the server. ' + + 'Please provide a request object to the expression execution params.', + }) + ); + } + + const { + aggs, + deserializeFieldFormat, + indexPatterns, + searchSource, + } = await getStartDependencies(kibanaRequest); + + const aggConfigsState = JSON.parse(args.aggConfigs); + const indexPattern = await indexPatterns.get(args.index); + const aggConfigs = aggs.createAggConfigs(indexPattern, aggConfigsState); + + return await handleEsaggsRequest(input, args, { + abortSignal: (abortSignal as unknown) as AbortSignal, + aggs: aggConfigs, + deserializeFieldFormat, + filters: get(input, 'filters', undefined), + indexPattern, + inspectorAdapters: inspectorAdapters as Adapters, + metricsAtAllLevels: args.metricsAtAllLevels, + partialRows: args.partialRows, + query: get(input, 'query', undefined) as any, + searchSessionId: getSearchSessionId(), + searchSourceService: searchSource, + timeFields: args.timeFields, + timeRange: get(input, 'timeRange', undefined), + }); + }, + }); +} + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getEsaggs({ + getStartServices, +}: { + getStartServices: StartServicesAccessor<DataPluginStartDependencies, DataPluginStart>; +}): () => EsaggsExpressionFunctionDefinition { + return getFunctionDefinition({ + getStartDependencies: async (request: KibanaRequest) => { + const [{ elasticsearch, savedObjects, uiSettings }, , self] = await getStartServices(); + const { fieldFormats, indexPatterns, search } = self; + const esClient = elasticsearch.client.asScoped(request); + const savedObjectsClient = savedObjects.getScopedClient(request); + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const scopedFieldFormats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + return { + aggs: await search.aggs.asScopedToClient(savedObjectsClient, esClient.asCurrentUser), + deserializeFieldFormat: scopedFieldFormats.deserialize.bind(scopedFieldFormats), + indexPatterns: await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + esClient.asCurrentUser + ), + searchSource: await search.searchSource.asScoped(request), + }; + }, + }); +} diff --git a/src/plugins/data/server/search/expressions/index.ts b/src/plugins/data/server/search/expressions/index.ts new file mode 100644 index 0000000000000..f1a39a8383629 --- /dev/null +++ b/src/plugins/data/server/search/expressions/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './esaggs'; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a9539a8fd3c15..f0c6b383b27e9 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -65,6 +65,7 @@ import { searchSourceRequiredUiSettings, SearchSourceService, } from '../../common/search'; +import { getEsaggs } from './expressions'; import { getShardDelayBucketAgg, SHARD_DELAY_AGG_NAME, @@ -195,6 +196,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { registerUsageCollector(usageCollection, this.initializerContext); } + expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices })); expressions.registerFunction(kibana); expressions.registerFunction(kibanaContextFunction); expressions.registerType(kibanaContext); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 86ec784834ace..fd1f17b20a514 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -29,7 +29,7 @@ import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; -import { FormatFactory } from 'src/plugins/data/common/field_formats/utils'; +import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; import { ISavedObjectsRepository } from 'src/core/server'; import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -733,8 +733,11 @@ export class IndexPatternsFetcher { // // @public (undocumented) export class IndexPatternsService implements Plugin_3<void, IndexPatternsServiceStart> { + // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts + // // (undocumented) - setup(core: CoreSetup_2): void; + setup(core: CoreSetup_2<DataPluginStartDependencies, PluginStart>, { expressions }: IndexPatternsServiceSetupDeps): void; // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -942,7 +945,6 @@ export type ParsedInterval = ReturnType<typeof parseEsInterval>; export function parseInterval(interval: string): moment.Duration | null; // Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DataServerPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1250,7 +1252,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index_patterns/index_patterns_service.ts:70:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:90:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 3bd29632f0902..10a18d0cbf435 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -195,6 +195,18 @@ describe('Execution', () => { expect(typeof result).toBe('object'); }); + test('context.getKibanaRequest is a function if provided', async () => { + const { result } = (await run('introspectContext key="getKibanaRequest"', { + kibanaRequest: {}, + })) as any; + expect(typeof result).toBe('function'); + }); + + test('context.getKibanaRequest is undefined if not provided', async () => { + const { result } = (await run('introspectContext key="getKibanaRequest"')) as any; + expect(typeof result).toBe('undefined'); + }); + test('unknown context key is undefined', async () => { const { result } = (await run('introspectContext key="foo"')) as any; expect(typeof result).toBe('undefined'); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index e53a6f7d58e1c..9eae7fd717eda 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -152,6 +152,9 @@ export class Execution< this.context = { getSearchContext: () => this.execution.params.searchContext || {}, getSearchSessionId: () => execution.params.searchSessionId, + getKibanaRequest: execution.params.kibanaRequest + ? () => execution.params.kibanaRequest + : undefined, variables: execution.params.variables || {}, types: executor.getTypes(), abortSignal: this.abortController.signal, diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index abe3e08fc20c2..a41f97118c4b2 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -17,6 +17,9 @@ * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { KibanaRequest } from 'src/core/server'; + import { ExpressionType, SerializableState } from '../expression_types'; import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common'; import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; @@ -59,6 +62,13 @@ export interface ExecutionContext< */ getSearchSessionId: () => string | undefined; + /** + * Getter to retrieve the `KibanaRequest` object inside an expression function. + * Useful for functions which are running on the server and need to perform + * operations that are scoped to a specific user. + */ + getKibanaRequest?: () => KibanaRequest; + /** * Allows to fetch saved objects from ElasticSearch. In browser `getSavedObject` * function is provided automatically by the Expressions plugin. On the server diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index c9cc0680360bb..ec1fffe64f102 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -17,6 +17,9 @@ * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { KibanaRequest } from 'src/core/server'; + import { Executor } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; import { ExpressionAstExpression } from '../ast'; @@ -58,6 +61,13 @@ export interface ExpressionExecutionParams { */ debug?: boolean; + /** + * Makes a `KibanaRequest` object available to expression functions. Useful for + * functions which are running on the server and need to perform operations that + * are scoped to a specific user. + */ + kibanaRequest?: KibanaRequest; + searchSessionId?: string; inspectorAdapters?: Adapters; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 2a73cd6e208d1..97ff00db0966c 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -9,6 +9,7 @@ import { CoreStart } from 'src/core/public'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { EventEmitter } from 'events'; +import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { PersistedState } from 'src/plugins/visualizations/public'; @@ -136,6 +137,7 @@ export type ExecutionContainer<Output = ExpressionValue> = StateContainer<Execut // @public export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters, ExecutionContextSearch extends SerializableState_2 = SerializableState_2> { abortSignal: AbortSignal; + getKibanaRequest?: () => KibanaRequest; // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts getSavedObject?: <T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>>; diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 33ff759faa3b1..761ddba8f9270 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/server'; import { CoreStart } from 'src/core/server'; import { Ensure } from '@kbn/utility-types'; import { EventEmitter } from 'events'; +import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; import { PersistedState } from 'src/plugins/visualizations/public'; import { Plugin as Plugin_2 } from 'src/core/server'; @@ -134,6 +135,7 @@ export type ExecutionContainer<Output = ExpressionValue> = StateContainer<Execut // @public export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters, ExecutionContextSearch extends SerializableState_2 = SerializableState_2> { abortSignal: AbortSignal; + getKibanaRequest?: () => KibanaRequest; // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts getSavedObject?: <T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>>; From 0bd928184cfde2cebe5d8429002c320e3a04f65d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 3 Dec 2020 10:56:41 -0500 Subject: [PATCH 104/107] y18n 4.0.0 -> 4.0.1 (#84905) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 73741371d10c7..172cf043dbbee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29800,9 +29800,9 @@ y18n@^3.2.0, y18n@^3.2.1: integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== y18n@^5.0.1: version "5.0.5" From ac71d2e9411d1936c2a94a5cb2f71a1e47cd4708 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 3 Dec 2020 10:57:27 -0500 Subject: [PATCH 105/107] [SECURITY_SOLUTION] delete advanced Policy fields when they are empty (#84368) --- .../pages/policy/view/policy_advanced.tsx | 32 ++ .../apps/endpoint/policy_details.ts | 281 ++++++++++++++++++ 2 files changed, 313 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index e4e03e9453f7a..c6b677110315a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -15,6 +15,8 @@ import { AdvancedPolicySchema } from '../models/advanced_policy_schema'; function setValue(obj: Record<string, unknown>, value: string, path: string[]) { let newPolicyConfig = obj; + + // First set the value. for (let i = 0; i < path.length - 1; i++) { if (!newPolicyConfig[path[i]]) { newPolicyConfig[path[i]] = {} as Record<string, unknown>; @@ -22,6 +24,36 @@ function setValue(obj: Record<string, unknown>, value: string, path: string[]) { newPolicyConfig = newPolicyConfig[path[i]] as Record<string, unknown>; } newPolicyConfig[path[path.length - 1]] = value; + + // Then, if the user is deleting the value, we need to ensure we clean up the config. + // We delete any sections that are empty, whether that be an empty string, empty object, or undefined. + if (value === '' || value === undefined) { + newPolicyConfig = obj; + for (let k = path.length; k >= 0; k--) { + const nextPath = path.slice(0, k); + for (let i = 0; i < nextPath.length - 1; i++) { + // Traverse and find the next section + newPolicyConfig = newPolicyConfig[nextPath[i]] as Record<string, unknown>; + } + if ( + newPolicyConfig[nextPath[nextPath.length - 1]] === undefined || + newPolicyConfig[nextPath[nextPath.length - 1]] === '' || + Object.keys(newPolicyConfig[nextPath[nextPath.length - 1]] as object).length === 0 + ) { + // If we're looking at the `advanced` field, we leave it undefined as opposed to deleting it. + // This is because the UI looks for this field to begin rendering. + if (nextPath[nextPath.length - 1] === 'advanced') { + newPolicyConfig[nextPath[nextPath.length - 1]] = undefined; + // In all other cases, if field is empty, we'll delete it to clean up. + } else { + delete newPolicyConfig[nextPath[nextPath.length - 1]]; + } + newPolicyConfig = obj; + } else { + break; // We are looking at a non-empty section, so we can terminate. + } + } + } } function getValue(obj: Record<string, unknown>, path: string[]) { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 166fc39f4aaaa..355e494cb459e 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -254,6 +254,287 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, ]); }); + + it('should have cleared the advanced section when the user deletes the value', async () => { + const advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); + await advancedPolicyButton.click(); + + const advancedPolicyField = await pageObjects.policy.findAdvancedPolicyField(); + await advancedPolicyField.clearValue(); + await advancedPolicyField.click(); + await advancedPolicyField.type('true'); + await pageObjects.policy.confirmAndSave(); + + await testSubjects.existOrFail('policyDetailsSuccessMessage'); + + const agentFullPolicy = await policyTestResources.getFullAgentPolicy( + policyInfo.agentPolicy.id + ); + + expect(agentFullPolicy.inputs).to.eql([ + { + id: policyInfo.packagePolicy.id, + revision: 2, + data_stream: { namespace: 'default' }, + name: 'Protect East Coast', + meta: { + package: { + name: 'endpoint', + version: policyInfo.packageInfo.version, + }, + }, + artifact_manifest: { + artifacts: { + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + // The manifest version could have changed when the Policy was updated because the + // policy details page ensures that a save action applies the udpated policy on top + // of the latest Package Policy. So we just ignore the check against this value by + // forcing it to be the same as the value returned in the full agent policy. + manifest_version: agentFullPolicy.inputs[0].artifact_manifest.manifest_version, + schema_version: 'v1', + }, + policy: { + linux: { + events: { file: true, network: true, process: true }, + logging: { file: 'info' }, + advanced: { agent: { connection_delay: 'true' } }, + }, + mac: { + events: { file: true, network: true, process: true }, + logging: { file: 'info' }, + malware: { mode: 'prevent' }, + popup: { + malware: { + enabled: true, + message: 'Elastic Security { action } { filename }', + }, + }, + }, + windows: { + events: { + dll_and_driver_load: true, + dns: true, + file: true, + network: true, + process: true, + registry: true, + security: true, + }, + logging: { file: 'info' }, + malware: { mode: 'prevent' }, + popup: { + malware: { + enabled: true, + message: 'Elastic Security { action } { filename }', + }, + }, + antivirus_registration: { + enabled: false, + }, + }, + }, + type: 'endpoint', + use_output: 'default', + }, + ]); + + // Clear the value + await advancedPolicyField.click(); + await advancedPolicyField.clearValueWithKeyboard(); + await pageObjects.policy.confirmAndSave(); + + await testSubjects.existOrFail('policyDetailsSuccessMessage'); + + const agentFullPolicyUpdated = await policyTestResources.getFullAgentPolicy( + policyInfo.agentPolicy.id + ); + + expect(agentFullPolicyUpdated.inputs).to.eql([ + { + id: policyInfo.packagePolicy.id, + revision: 3, + data_stream: { namespace: 'default' }, + name: 'Protect East Coast', + meta: { + package: { + name: 'endpoint', + version: policyInfo.packageInfo.version, + }, + }, + artifact_manifest: { + artifacts: { + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + // The manifest version could have changed when the Policy was updated because the + // policy details page ensures that a save action applies the udpated policy on top + // of the latest Package Policy. So we just ignore the check against this value by + // forcing it to be the same as the value returned in the full agent policy. + manifest_version: agentFullPolicy.inputs[0].artifact_manifest.manifest_version, + schema_version: 'v1', + }, + policy: { + linux: { + events: { file: true, network: true, process: true }, + logging: { file: 'info' }, + }, + mac: { + events: { file: true, network: true, process: true }, + logging: { file: 'info' }, + malware: { mode: 'prevent' }, + popup: { + malware: { + enabled: true, + message: 'Elastic Security { action } { filename }', + }, + }, + }, + windows: { + events: { + dll_and_driver_load: true, + dns: true, + file: true, + network: true, + process: true, + registry: true, + security: true, + }, + logging: { file: 'info' }, + malware: { mode: 'prevent' }, + popup: { + malware: { + enabled: true, + message: 'Elastic Security { action } { filename }', + }, + }, + antivirus_registration: { + enabled: false, + }, + }, + }, + type: 'endpoint', + use_output: 'default', + }, + ]); + }); }); describe('when on Ingest Policy Edit Package Policy page', async () => { From c39d14fef42186b7330133bad1d107b0754aa693 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 3 Dec 2020 11:08:25 -0500 Subject: [PATCH 106/107] Disable checking for conflicts when copying saved objects (#83575) --- .../copy_saved_objects.asciidoc | 20 +- ...olve_copy_saved_objects_conflicts.asciidoc | 6 + ...savedobjectsrepository.incrementcounter.md | 4 +- src/core/server/core_usage_data/constants.ts | 24 ++ .../core_usage_data_service.mock.ts | 14 +- .../core_usage_data_service.test.ts | 68 +++++- .../core_usage_data_service.ts | 46 +++- .../core_usage_data/core_usage_stats.ts | 32 +++ .../core_usage_stats_client.mock.ts | 32 +++ .../core_usage_stats_client.test.ts | 227 ++++++++++++++++++ .../core_usage_stats_client.ts | 154 ++++++++++++ src/core/server/core_usage_data/index.ts | 12 +- src/core/server/core_usage_data/types.ts | 39 ++- src/core/server/index.ts | 9 +- .../server/saved_objects/routes/export.ts | 17 +- .../server/saved_objects/routes/import.ts | 18 +- src/core/server/saved_objects/routes/index.ts | 9 +- .../routes/integration_tests/export.test.ts | 16 +- .../routes/integration_tests/import.test.ts | 16 +- .../resolve_import_errors.test.ts | 17 +- .../routes/resolve_import_errors.ts | 21 +- .../saved_objects_service.test.ts | 2 + .../saved_objects/saved_objects_service.ts | 10 +- .../saved_objects/service/lib/repository.ts | 4 +- src/core/server/server.api.md | 42 +++- src/core/server/server.test.ts | 1 + src/core/server/server.ts | 23 +- .../collectors/core/core_usage_collector.ts | 17 ++ .../__snapshots__/flyout.test.tsx.snap | 16 +- .../objects_table/components/flyout.tsx | 12 +- .../components/import_mode_control.tsx | 2 +- src/plugins/telemetry/schema/oss_plugins.json | 51 ++++ .../components/copy_mode_control.test.tsx | 24 +- .../components/copy_mode_control.tsx | 18 +- .../components/copy_to_space_flyout.test.tsx | 20 +- .../components/copy_to_space_flyout.tsx | 2 +- .../components/copy_to_space_form.tsx | 16 +- .../components/selectable_spaces_control.tsx | 2 +- .../components/selectable_spaces_control.tsx | 2 +- .../components/share_to_space_form.tsx | 2 +- .../public/spaces_manager/spaces_manager.ts | 3 +- x-pack/plugins/spaces/server/plugin.ts | 10 +- .../routes/api/external/copy_to_space.test.ts | 49 ++++ .../routes/api/external/copy_to_space.ts | 37 ++- .../server/routes/api/external/delete.test.ts | 4 + .../server/routes/api/external/get.test.ts | 4 + .../routes/api/external/get_all.test.ts | 4 + .../server/routes/api/external/index.ts | 4 +- .../server/routes/api/external/post.test.ts | 4 + .../server/routes/api/external/put.test.ts | 4 + .../api/external/share_to_space.test.ts | 4 + .../spaces/server/saved_objects/mappings.ts | 5 + .../saved_objects_service.test.ts | 55 +---- .../saved_objects/saved_objects_service.ts | 10 +- .../spaces_usage_collector.test.ts | 114 ++++++--- .../spaces_usage_collector.ts | 45 +++- .../spaces/server/usage_stats/constants.ts | 8 + .../spaces/server/usage_stats/index.ts | 9 + .../spaces/server/usage_stats/types.ts | 20 ++ .../usage_stats/usage_stats_client.mock.ts | 18 ++ .../usage_stats/usage_stats_client.test.ts | 181 ++++++++++++++ .../server/usage_stats/usage_stats_client.ts | 108 +++++++++ .../usage_stats/usage_stats_service.mock.ts | 19 ++ .../usage_stats/usage_stats_service.test.ts | 39 +++ .../server/usage_stats/usage_stats_service.ts | 36 +++ .../schema/xpack_plugins.json | 36 +++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apps/spaces/copy_saved_objects.ts | 32 +++ .../copy_saved_objects_to_space_page.ts | 12 + .../common/suites/copy_to_space.ts | 9 +- .../suites/resolve_copy_to_space_conflicts.ts | 8 +- 72 files changed, 1769 insertions(+), 191 deletions(-) create mode 100644 src/core/server/core_usage_data/constants.ts create mode 100644 src/core/server/core_usage_data/core_usage_stats.ts create mode 100644 src/core/server/core_usage_data/core_usage_stats_client.mock.ts create mode 100644 src/core/server/core_usage_data/core_usage_stats_client.test.ts create mode 100644 src/core/server/core_usage_data/core_usage_stats_client.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/constants.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/index.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/types.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/usage_stats_client.mock.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/usage_stats_service.mock.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/usage_stats_service.test.ts create mode 100644 x-pack/plugins/spaces/server/usage_stats/usage_stats_service.ts diff --git a/docs/api/spaces-management/copy_saved_objects.asciidoc b/docs/api/spaces-management/copy_saved_objects.asciidoc index 853cca035a291..1dd9cc9734a52 100644 --- a/docs/api/spaces-management/copy_saved_objects.asciidoc +++ b/docs/api/spaces-management/copy_saved_objects.asciidoc @@ -51,9 +51,17 @@ You can request to overwrite any objects that already exist in the target space (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target spaces. The default value is `false`. +`createNewCopies`:: + (Optional, boolean) Creates new copies of saved objects, regenerates each object ID, and resets the origin. When used, potential conflict + errors are avoided. The default value is `true`. ++ +NOTE: This cannot be used with the `overwrite` option. + `overwrite`:: (Optional, boolean) When set to `true`, all conflicts are automatically overidden. When a saved object with a matching `type` and `id` exists in the target space, that version is replaced with the version from the source space. The default value is `false`. ++ +NOTE: This cannot be used with the `createNewCopies` option. [role="child_attributes"] [[spaces-api-copy-saved-objects-response-body]] @@ -128,8 +136,7 @@ $ curl -X POST api/spaces/_copy_saved_objects "id": "my-dashboard" }], "spaces": ["marketing"], - "includeReferences": true, - "createNewcopies": true + "includeReferences": true } ---- // KIBANA @@ -193,7 +200,8 @@ $ curl -X POST api/spaces/_copy_saved_objects "id": "my-dashboard" }], "spaces": ["marketing"], - "includeReferences": true + "includeReferences": true, + "createNewCopies": false } ---- // KIBANA @@ -254,7 +262,8 @@ $ curl -X POST api/spaces/_copy_saved_objects "id": "my-dashboard" }], "spaces": ["marketing", "sales"], - "includeReferences": true + "includeReferences": true, + "createNewCopies": false } ---- // KIBANA @@ -405,7 +414,8 @@ $ curl -X POST api/spaces/_copy_saved_objects "id": "my-dashboard" }], "spaces": ["marketing"], - "includeReferences": true + "includeReferences": true, + "createNewCopies": false } ---- // KIBANA diff --git a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc index 6d799ebb0014e..1a0017fe167ab 100644 --- a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc +++ b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc @@ -45,6 +45,10 @@ Execute the <<spaces-api-copy-saved-objects,copy saved objects to space API>>, w `includeReferences`:: (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects are copied into the target spaces. The `includeReferences` must be the same values used during the failed <<spaces-api-copy-saved-objects, copy saved objects to space API>> operation. The default value is `false`. +`createNewCopies`:: + (Optional, boolean) Creates new copies of the saved objects, regenerates each object ID, and resets the origin. When enabled during the + initial copy, also enable when resolving copy errors. The default value is `true`. + `retries`:: (Required, object) The retry operations to attempt, which can specify how to resolve different types of errors. Object keys represent the target space IDs. @@ -148,6 +152,7 @@ $ curl -X POST api/spaces/_resolve_copy_saved_objects_errors "id": "my-dashboard" }], "includeReferences": true, + "createNewCopies": false, "retries": { "sales": [ { @@ -246,6 +251,7 @@ $ curl -X POST api/spaces/_resolve_copy_saved_objects_errors "id": "my-dashboard" }], "includeReferences": true, + "createNewCopies": false, "retries": { "marketing": [ { diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index dc62cacf6741b..f4e35d532f235 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -9,7 +9,7 @@ Increments all the specified counter fields by one. Creates the document if one <b>Signature:</b> ```typescript -incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>; +incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>; ``` ## Parameters @@ -23,7 +23,7 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options? <b>Returns:</b> -`Promise<SavedObject>` +`Promise<SavedObject<T>>` The saved object after the specified fields were incremented diff --git a/src/core/server/core_usage_data/constants.ts b/src/core/server/core_usage_data/constants.ts new file mode 100644 index 0000000000000..0bae7a8cad9d2 --- /dev/null +++ b/src/core/server/core_usage_data/constants.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export const CORE_USAGE_STATS_TYPE = 'core-usage-stats'; + +/** @internal */ +export const CORE_USAGE_STATS_ID = 'core-usage-stats'; diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index b1c731e8ba534..9501386318cad 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -20,7 +20,16 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import { CoreUsageDataService } from './core_usage_data_service'; -import { CoreUsageData, CoreUsageDataStart } from './types'; +import { coreUsageStatsClientMock } from './core_usage_stats_client.mock'; +import { CoreUsageData, CoreUsageDataSetup, CoreUsageDataStart } from './types'; + +const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => { + const setupContract: jest.Mocked<CoreUsageDataSetup> = { + registerType: jest.fn(), + getClient: jest.fn().mockReturnValue(usageStatsClient), + }; + return setupContract; +}; const createStartContractMock = () => { const startContract: jest.Mocked<CoreUsageDataStart> = { @@ -140,7 +149,7 @@ const createStartContractMock = () => { const createMock = () => { const mocked: jest.Mocked<PublicMethodsOf<CoreUsageDataService>> = { - setup: jest.fn(), + setup: jest.fn().mockReturnValue(createSetupContractMock()), start: jest.fn().mockReturnValue(createStartContractMock()), stop: jest.fn(), }; @@ -149,5 +158,6 @@ const createMock = () => { export const coreUsageDataServiceMock = { create: createMock, + createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, }; diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 6686a778ee8a5..e22dfcb1e3a20 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -34,6 +34,9 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service. import { CoreUsageDataService } from './core_usage_data_service'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; +import { typeRegistryMock } from '../saved_objects/saved_objects_type_registry.mock'; +import { CORE_USAGE_STATS_TYPE } from './constants'; +import { CoreUsageStatsClient } from './core_usage_stats_client'; describe('CoreUsageDataService', () => { const getTestScheduler = () => @@ -63,11 +66,67 @@ describe('CoreUsageDataService', () => { service = new CoreUsageDataService(coreContext); }); + describe('setup', () => { + it('creates internal repository', async () => { + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + service.setup({ metrics, savedObjectsStartPromise }); + + const savedObjects = await savedObjectsStartPromise; + expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1); + expect(savedObjects.createInternalRepository).toHaveBeenCalledWith([CORE_USAGE_STATS_TYPE]); + }); + + describe('#registerType', () => { + it('registers core usage stats type', async () => { + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + const coreUsageData = service.setup({ + metrics, + savedObjectsStartPromise, + }); + const typeRegistry = typeRegistryMock.create(); + + coreUsageData.registerType(typeRegistry); + expect(typeRegistry.registerType).toHaveBeenCalledTimes(1); + expect(typeRegistry.registerType).toHaveBeenCalledWith({ + name: CORE_USAGE_STATS_TYPE, + hidden: true, + namespaceType: 'agnostic', + mappings: expect.anything(), + }); + }); + }); + + describe('#getClient', () => { + it('returns client', async () => { + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + const coreUsageData = service.setup({ + metrics, + savedObjectsStartPromise, + }); + + const usageStatsClient = coreUsageData.getClient(); + expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient); + }); + }); + }); + describe('start', () => { describe('getCoreUsageData', () => { - it('returns core metrics for default config', () => { + it('returns core metrics for default config', async () => { const metrics = metricsServiceMock.createInternalSetupContract(); - service.setup({ metrics }); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + service.setup({ metrics, savedObjectsStartPromise }); const elasticsearch = elasticsearchServiceMock.createStart(); elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ body: [ @@ -243,8 +302,11 @@ describe('CoreUsageDataService', () => { observables.push(newObservable); return newObservable as Observable<any>; }); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); - service.setup({ metrics }); + service.setup({ metrics, savedObjectsStartPromise }); // Use the stopTimer$ to delay calling stop() until the third frame const stopTimer$ = cold('---a|'); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 490c411ecb852..02b4f2ac59133 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -21,20 +21,29 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { CoreService } from 'src/core/types'; -import { SavedObjectsServiceStart } from 'src/core/server'; +import { Logger, SavedObjectsServiceStart, SavedObjectTypeRegistry } from 'src/core/server'; import { CoreContext } from '../core_context'; import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; import { HttpConfigType } from '../http'; import { LoggingConfigType } from '../logging'; import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; -import { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart } from './types'; +import { + CoreServicesUsageData, + CoreUsageData, + CoreUsageDataStart, + CoreUsageDataSetup, +} from './types'; import { isConfigured } from './is_configured'; import { ElasticsearchServiceStart } from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; +import { coreUsageStatsType } from './core_usage_stats'; +import { CORE_USAGE_STATS_TYPE } from './constants'; +import { CoreUsageStatsClient } from './core_usage_stats_client'; import { MetricsServiceSetup, OpsMetrics } from '..'; export interface SetupDeps { metrics: MetricsServiceSetup; + savedObjectsStartPromise: Promise<SavedObjectsServiceStart>; } export interface StartDeps { @@ -60,7 +69,8 @@ const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => { return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager'; }; -export class CoreUsageDataService implements CoreService<void, CoreUsageDataStart> { +export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, CoreUsageDataStart> { + private logger: Logger; private elasticsearchConfig?: ElasticsearchConfigType; private configService: CoreContext['configService']; private httpConfig?: HttpConfigType; @@ -69,8 +79,10 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar private stop$: Subject<void>; private opsMetrics?: OpsMetrics; private kibanaConfig?: KibanaConfigType; + private coreUsageStatsClient?: CoreUsageStatsClient; constructor(core: CoreContext) { + this.logger = core.logger.get('core-usage-stats-service'); this.configService = core.configService; this.stop$ = new Subject(); } @@ -130,8 +142,15 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar throw new Error('Unable to read config values. Ensure that setup() has completed.'); } + if (!this.coreUsageStatsClient) { + throw new Error( + 'Core usage stats client is not initialized. Ensure that setup() has completed.' + ); + } + const es = this.elasticsearchConfig; const soUsageData = await this.getSavedObjectIndicesUsageData(savedObjects, elasticsearch); + const coreUsageStatsData = await this.coreUsageStatsClient.getUsageStats(); const http = this.httpConfig; return { @@ -225,10 +244,11 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar services: { savedObjects: soUsageData, }, + ...coreUsageStatsData, }; } - setup({ metrics }: SetupDeps) { + setup({ metrics, savedObjectsStartPromise }: SetupDeps) { metrics .getOpsMetrics$() .pipe(takeUntil(this.stop$)) @@ -268,6 +288,24 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar .subscribe((config) => { this.kibanaConfig = config; }); + + const internalRepositoryPromise = savedObjectsStartPromise.then((savedObjects) => + savedObjects.createInternalRepository([CORE_USAGE_STATS_TYPE]) + ); + + const registerType = (typeRegistry: SavedObjectTypeRegistry) => { + typeRegistry.registerType(coreUsageStatsType); + }; + + const getClient = () => { + const debugLogger = (message: string) => this.logger.debug(message); + + return new CoreUsageStatsClient(debugLogger, internalRepositoryPromise); + }; + + this.coreUsageStatsClient = getClient(); + + return { registerType, getClient } as CoreUsageDataSetup; } start({ savedObjects, elasticsearch }: StartDeps) { diff --git a/src/core/server/core_usage_data/core_usage_stats.ts b/src/core/server/core_usage_data/core_usage_stats.ts new file mode 100644 index 0000000000000..382a544a58960 --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_stats.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsType } from '../saved_objects'; +import { CORE_USAGE_STATS_TYPE } from './constants'; + +/** @internal */ +export const coreUsageStatsType: SavedObjectsType = { + name: CORE_USAGE_STATS_TYPE, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields + properties: {}, + }, +}; diff --git a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts new file mode 100644 index 0000000000000..3bfb411c9dd49 --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreUsageStatsClient } from '.'; + +const createUsageStatsClientMock = () => + (({ + getUsageStats: jest.fn().mockResolvedValue({}), + incrementSavedObjectsImport: jest.fn().mockResolvedValue(null), + incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null), + incrementSavedObjectsExport: jest.fn().mockResolvedValue(null), + } as unknown) as jest.Mocked<CoreUsageStatsClient>); + +export const coreUsageStatsClientMock = { + create: createUsageStatsClientMock, +}; diff --git a/src/core/server/core_usage_data/core_usage_stats_client.test.ts b/src/core/server/core_usage_data/core_usage_stats_client.test.ts new file mode 100644 index 0000000000000..e4f47667fce6b --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_stats_client.test.ts @@ -0,0 +1,227 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { savedObjectsRepositoryMock } from '../mocks'; +import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; +import { + IncrementSavedObjectsImportOptions, + IncrementSavedObjectsResolveImportErrorsOptions, + IncrementSavedObjectsExportOptions, + IMPORT_STATS_PREFIX, + RESOLVE_IMPORT_STATS_PREFIX, + EXPORT_STATS_PREFIX, +} from './core_usage_stats_client'; +import { CoreUsageStatsClient } from '.'; + +describe('CoreUsageStatsClient', () => { + const setup = () => { + const debugLoggerMock = jest.fn(); + const repositoryMock = savedObjectsRepositoryMock.create(); + const usageStatsClient = new CoreUsageStatsClient( + debugLoggerMock, + Promise.resolve(repositoryMock) + ); + return { usageStatsClient, debugLoggerMock, repositoryMock }; + }; + + const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request + const incrementOptions = { refresh: false }; + + describe('#getUsageStats', () => { + it('returns empty object when encountering a repository error', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.get.mockRejectedValue(new Error('Oh no!')); + + const result = await usageStatsClient.getUsageStats(); + expect(result).toEqual({}); + }); + + it('returns object attributes when usage stats exist', async () => { + const { usageStatsClient, repositoryMock } = setup(); + const usageStats = { foo: 'bar' }; + repositoryMock.incrementCounter.mockResolvedValue({ + type: CORE_USAGE_STATS_TYPE, + id: CORE_USAGE_STATS_ID, + attributes: usageStats, + references: [], + }); + + const result = await usageStatsClient.getUsageStats(); + expect(result).toEqual(usageStats); + }); + }); + + describe('#incrementSavedObjectsImport', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + await expect( + usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${IMPORT_STATS_PREFIX}.total`, + `${IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementSavedObjectsImport({ + headers: firstPartyRequestHeaders, + createNewCopies: true, + overwrite: true, + } as IncrementSavedObjectsImportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${IMPORT_STATS_PREFIX}.total`, + `${IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsResolveImportErrors', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + await expect( + usageStatsClient.incrementSavedObjectsResolveImportErrors( + {} as IncrementSavedObjectsResolveImportErrorsOptions + ) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementSavedObjectsResolveImportErrors( + {} as IncrementSavedObjectsResolveImportErrorsOptions + ); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_IMPORT_STATS_PREFIX}.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementSavedObjectsResolveImportErrors({ + headers: firstPartyRequestHeaders, + createNewCopies: true, + } as IncrementSavedObjectsResolveImportErrorsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_IMPORT_STATS_PREFIX}.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsExport', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + await expect( + usageStatsClient.incrementSavedObjectsExport({} as IncrementSavedObjectsExportOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementSavedObjectsExport({ + types: undefined, + supportedTypes: ['foo', 'bar'], + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${EXPORT_STATS_PREFIX}.total`, + `${EXPORT_STATS_PREFIX}.kibanaRequest.no`, + `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementSavedObjectsExport({ + headers: firstPartyRequestHeaders, + types: ['foo', 'bar'], + supportedTypes: ['foo', 'bar'], + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${EXPORT_STATS_PREFIX}.total`, + `${EXPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, + ], + incrementOptions + ); + }); + }); +}); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts new file mode 100644 index 0000000000000..58356832d8b8a --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -0,0 +1,154 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; +import { CoreUsageStats } from './types'; +import { + Headers, + ISavedObjectsRepository, + SavedObjectsImportOptions, + SavedObjectsResolveImportErrorsOptions, + SavedObjectsExportOptions, +} from '..'; + +interface BaseIncrementOptions { + headers?: Headers; +} +/** @internal */ +export type IncrementSavedObjectsImportOptions = BaseIncrementOptions & + Pick<SavedObjectsImportOptions, 'createNewCopies' | 'overwrite'>; +/** @internal */ +export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptions & + Pick<SavedObjectsResolveImportErrorsOptions, 'createNewCopies'>; +/** @internal */ +export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & + Pick<SavedObjectsExportOptions, 'types'> & { supportedTypes: string[] }; + +export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport'; +export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors'; +export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport'; +const ALL_COUNTER_FIELDS = [ + `${IMPORT_STATS_PREFIX}.total`, + `${IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, + `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${EXPORT_STATS_PREFIX}.total`, + `${EXPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${EXPORT_STATS_PREFIX}.kibanaRequest.no`, + `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, + `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, +]; + +/** @internal */ +export class CoreUsageStatsClient { + constructor( + private readonly debugLogger: (message: string) => void, + private readonly repositoryPromise: Promise<ISavedObjectsRepository> + ) {} + + public async getUsageStats() { + this.debugLogger('getUsageStats() called'); + let coreUsageStats: CoreUsageStats = {}; + try { + const repository = await this.repositoryPromise; + const result = await repository.incrementCounter<CoreUsageStats>( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + ALL_COUNTER_FIELDS, + { initialize: true } // set all counter fields to 0 if they don't exist + ); + coreUsageStats = result.attributes; + } catch (err) { + // do nothing + } + return coreUsageStats; + } + + public async incrementSavedObjectsImport({ + headers, + createNewCopies, + overwrite, + }: IncrementSavedObjectsImportOptions) { + const isKibanaRequest = getIsKibanaRequest(headers); + const counterFieldNames = [ + 'total', + `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, + `overwriteEnabled.${overwrite ? 'yes' : 'no'}`, + ]; + await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX); + } + + public async incrementSavedObjectsResolveImportErrors({ + headers, + createNewCopies, + }: IncrementSavedObjectsResolveImportErrorsOptions) { + const isKibanaRequest = getIsKibanaRequest(headers); + const counterFieldNames = [ + 'total', + `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, + ]; + await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX); + } + + public async incrementSavedObjectsExport({ + headers, + types, + supportedTypes, + }: IncrementSavedObjectsExportOptions) { + const isKibanaRequest = getIsKibanaRequest(headers); + const isAllTypesSelected = !!types && supportedTypes.every((x) => types.includes(x)); + const counterFieldNames = [ + 'total', + `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + `allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`, + ]; + await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX); + } + + private async updateUsageStats(counterFieldNames: string[], prefix: string) { + const options = { refresh: false }; + try { + const repository = await this.repositoryPromise; + await repository.incrementCounter( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + counterFieldNames.map((x) => `${prefix}.${x}`), + options + ); + } catch (err) { + // do nothing + } + } +} + +function getIsKibanaRequest(headers?: Headers) { + // The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client. + // We can't be 100% certain, but this is a reasonable attempt. + return headers && headers['kbn-version'] && headers.origin && headers.referer; +} diff --git a/src/core/server/core_usage_data/index.ts b/src/core/server/core_usage_data/index.ts index b78c126657ef6..95d88f165a976 100644 --- a/src/core/server/core_usage_data/index.ts +++ b/src/core/server/core_usage_data/index.ts @@ -16,16 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -export { CoreUsageDataStart } from './types'; +export { CoreUsageDataSetup, CoreUsageDataStart } from './types'; export { CoreUsageDataService } from './core_usage_data_service'; +export { CoreUsageStatsClient } from './core_usage_stats_client'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected import { + CoreUsageStats, CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData, } from './types'; -export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData }; +export { + CoreUsageStats, + CoreUsageData, + CoreConfigUsageData, + CoreEnvironmentUsageData, + CoreServicesUsageData, +}; diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 258f452cfa6ae..aa41d75e6f2d4 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -17,11 +17,40 @@ * under the License. */ +import { CoreUsageStatsClient } from './core_usage_stats_client'; +import { ISavedObjectTypeRegistry, SavedObjectTypeRegistry } from '..'; + +/** + * @internal + * + * CoreUsageStats are collected over time while Kibana is running. This is related to CoreUsageData, which is a superset of this that also + * includes point-in-time configuration information. + * */ +export interface CoreUsageStats { + 'apiCalls.savedObjectsImport.total'?: number; + 'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number; + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number; + 'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number; + 'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.total'?: number; + 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number; + 'apiCalls.savedObjectsExport.total'?: number; + 'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; + 'apiCalls.savedObjectsExport.allTypesSelected.no'?: number; +} + /** * Type describing Core's usage data payload * @internal */ -export interface CoreUsageData { +export interface CoreUsageData extends CoreUsageStats { config: CoreConfigUsageData; services: CoreServicesUsageData; environment: CoreEnvironmentUsageData; @@ -141,6 +170,14 @@ export interface CoreConfigUsageData { // }; } +/** @internal */ +export interface CoreUsageDataSetup { + registerType( + typeRegistry: ISavedObjectTypeRegistry & Pick<SavedObjectTypeRegistry, 'registerType'> + ): void; + getClient(): CoreUsageStatsClient; +} + /** * Internal API for getting Core's usage data payload. * diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 9e654ea1e2303..7ce5c29a7e18b 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -69,13 +69,20 @@ import { I18nServiceSetup } from './i18n'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected import { + CoreUsageStats, CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData, } from './core_usage_data'; -export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData }; +export { + CoreUsageStats, + CoreUsageData, + CoreConfigUsageData, + CoreEnvironmentUsageData, + CoreServicesUsageData, +}; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 05a91f4aa4c2c..387280d777eaa 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -22,11 +22,20 @@ import stringify from 'json-stable-stringify'; import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { exportSavedObjectsToStream } from '../export'; import { validateTypes, validateObjects } from './utils'; -export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) => { +interface RouteDependencies { + config: SavedObjectConfig; + coreUsageData: CoreUsageDataSetup; +} + +export const registerExportRoute = ( + router: IRouter, + { config, coreUsageData }: RouteDependencies +) => { const { maxImportExportSize } = config; const referenceSchema = schema.object({ @@ -95,6 +104,12 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) } } + const { headers } = req; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient + .incrementSavedObjectsExport({ headers, types, supportedTypes }) + .catch(() => {}); + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, types, diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 291da5a5f0183..27be710c0a92a 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -21,17 +21,26 @@ import { Readable } from 'stream'; import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; import { importSavedObjectsFromStream } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; import { createSavedObjectsStreamFromNdJson } from './utils'; +interface RouteDependencies { + config: SavedObjectConfig; + coreUsageData: CoreUsageDataSetup; +} + interface FileStream extends Readable { hapi: { filename: string; }; } -export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) => { +export const registerImportRoute = ( + router: IRouter, + { config, coreUsageData }: RouteDependencies +) => { const { maxImportExportSize, maxImportPayloadBytes } = config; router.post( @@ -65,6 +74,13 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) }, router.handleLegacyErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; + + const { headers } = req; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient + .incrementSavedObjectsImport({ headers, createNewCopies, overwrite }) + .catch(() => {}); + const file = req.body.file as FileStream; const fileExtension = extname(file.hapi.filename).toLowerCase(); if (fileExtension !== '.ndjson') { diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index fd57a9f3059e3..19154b8583654 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -18,6 +18,7 @@ */ import { InternalHttpServiceSetup } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; import { Logger } from '../../logging'; import { SavedObjectConfig } from '../saved_objects_config'; import { IKibanaMigrator } from '../migrations'; @@ -37,11 +38,13 @@ import { registerMigrateRoute } from './migrate'; export function registerRoutes({ http, + coreUsageData, logger, config, migratorPromise, }: { http: InternalHttpServiceSetup; + coreUsageData: CoreUsageDataSetup; logger: Logger; config: SavedObjectConfig; migratorPromise: Promise<IKibanaMigrator>; @@ -57,9 +60,9 @@ export function registerRoutes({ registerBulkCreateRoute(router); registerBulkUpdateRoute(router); registerLogLegacyImportRoute(router, logger); - registerExportRoute(router, config); - registerImportRoute(router, config); - registerResolveImportErrorsRoute(router, config); + registerExportRoute(router, { config, coreUsageData }); + registerImportRoute(router, { config, coreUsageData }); + registerResolveImportErrorsRoute(router, { config, coreUsageData }); const internalRouter = http.createRouter('/internal/saved_objects/'); diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index 07bf320c29496..c37ed2da97681 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -25,6 +25,9 @@ import * as exportMock from '../../export'; import supertest from 'supertest'; import type { UnwrapPromise } from '@kbn/utility-types'; import { createListStream } from '@kbn/utils'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { SavedObjectConfig } from '../../saved_objects_config'; import { registerExportRoute } from '../export'; import { setupServer, createExportableType } from '../test_utils'; @@ -36,6 +39,7 @@ const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000, } as SavedObjectConfig; +let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>; describe('POST /api/saved_objects/_export', () => { let server: SetupServerReturn['server']; @@ -49,7 +53,10 @@ describe('POST /api/saved_objects/_export', () => { ); const router = httpSetup.createRouter('/api/saved_objects/'); - registerExportRoute(router, config); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsExport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the export does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerExportRoute(router, { config, coreUsageData }); await server.start(); }); @@ -59,7 +66,7 @@ describe('POST /api/saved_objects/_export', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const sortedObjects = [ { id: '1', @@ -110,5 +117,10 @@ describe('POST /api/saved_objects/_export', () => { types: ['search'], }) ); + expect(coreUsageStatsClient.incrementSavedObjectsExport).toHaveBeenCalledWith({ + headers: expect.anything(), + types: ['search'], + supportedTypes: ['index-pattern', 'search'], + }); }); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 34cd449f31963..9dfb7f79a925d 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -22,6 +22,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerImportRoute } from '../import'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { SavedObjectConfig } from '../../saved_objects_config'; import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectsErrorHelpers } from '../..'; @@ -31,6 +34,7 @@ type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>; const { v4: uuidv4 } = jest.requireActual('uuid'); const allowedTypes = ['index-pattern', 'visualization', 'dashboard']; const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig; +let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>; const URL = '/internal/saved_objects/_import'; describe(`POST ${URL}`, () => { @@ -71,7 +75,10 @@ describe(`POST ${URL}`, () => { savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); const router = httpSetup.createRouter('/internal/saved_objects/'); - registerImportRoute(router, config); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the import does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerImportRoute(router, { config, coreUsageData }); await server.start(); }); @@ -80,7 +87,7 @@ describe(`POST ${URL}`, () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const result = await supertest(httpSetup.server.listener) .post(URL) .set('content-Type', 'multipart/form-data; boundary=BOUNDARY') @@ -98,6 +105,11 @@ describe(`POST ${URL}`, () => { expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created + expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({ + headers: expect.anything(), + createNewCopies: false, + overwrite: false, + }); }); it('defaults migrationVersion to empty object', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 0e8fb0e563dbc..46f4d2435bf67 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -22,6 +22,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectConfig } from '../../saved_objects_config'; @@ -30,6 +33,7 @@ type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>; const { v4: uuidv4 } = jest.requireActual('uuid'); const allowedTypes = ['index-pattern', 'visualization', 'dashboard']; const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig; +let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>; const URL = '/api/saved_objects/_resolve_import_errors'; describe(`POST ${URL}`, () => { @@ -76,7 +80,12 @@ describe(`POST ${URL}`, () => { savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); const router = httpSetup.createRouter('/api/saved_objects/'); - registerResolveImportErrorsRoute(router, config); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue( + new Error('Oh no!') // this error is intentionally swallowed so the export does not fail + ); + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerResolveImportErrorsRoute(router, { config, coreUsageData }); await server.start(); }); @@ -85,7 +94,7 @@ describe(`POST ${URL}`, () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const result = await supertest(httpSetup.server.listener) .post(URL) .set('content-Type', 'multipart/form-data; boundary=BOUNDARY') @@ -107,6 +116,10 @@ describe(`POST ${URL}`, () => { expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created + expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({ + headers: expect.anything(), + createNewCopies: false, + }); }); it('defaults migrationVersion to empty object', async () => { diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 03b4322b27cbc..34c178a975304 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -21,17 +21,26 @@ import { extname } from 'path'; import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; import { resolveSavedObjectsImportErrors } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; import { createSavedObjectsStreamFromNdJson } from './utils'; +interface RouteDependencies { + config: SavedObjectConfig; + coreUsageData: CoreUsageDataSetup; +} + interface FileStream extends Readable { hapi: { filename: string; }; } -export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedObjectConfig) => { +export const registerResolveImportErrorsRoute = ( + router: IRouter, + { config, coreUsageData }: RouteDependencies +) => { const { maxImportExportSize, maxImportPayloadBytes } = config; router.post( @@ -72,6 +81,14 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO }, }, router.handleLegacyErrors(async (context, req, res) => { + const { createNewCopies } = req.query; + + const { headers } = req; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient + .incrementSavedObjectsResolveImportErrors({ headers, createNewCopies }) + .catch(() => {}); + const file = req.body.file as FileStream; const fileExtension = extname(file.hapi.filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -93,7 +110,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO readStream, retries: req.body.retries, objectLimit: maxImportExportSize, - createNewCopies: req.query.createNewCopies, + createNewCopies, }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 8e4c73137033d..c90f564ce33d7 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -33,6 +33,7 @@ import { Env } from '../config'; import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { elasticsearchClientMock } from '../elasticsearch/client/mocks'; +import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { httpServerMock } from '../http/http_server.mocks'; import { SavedObjectsClientFactoryProvider } from './service/lib'; @@ -64,6 +65,7 @@ describe('SavedObjectsService', () => { return { http: httpServiceMock.createInternalSetupContract(), elasticsearch: elasticsearchMock, + coreUsageData: coreUsageDataServiceMock.createSetupContract(), }; }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 5cc59d55a254e..400d3157bd00d 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -27,6 +27,7 @@ import { } from './'; import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; +import { CoreUsageDataSetup } from '../core_usage_data'; import { ElasticsearchClient, IClusterClient, @@ -253,6 +254,7 @@ export interface SavedObjectsRepositoryFactory { export interface SavedObjectsSetupDeps { http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; + coreUsageData: CoreUsageDataSetup; } interface WrappedClientFactoryWrapper { @@ -288,6 +290,7 @@ export class SavedObjectsService this.logger.debug('Setting up SavedObjects service'); this.setupDeps = setupDeps; + const { http, elasticsearch, coreUsageData } = setupDeps; const savedObjectsConfig = await this.coreContext.configService .atPath<SavedObjectsConfigType>('savedObjects') @@ -299,8 +302,11 @@ export class SavedObjectsService .toPromise(); this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig); + coreUsageData.registerType(this.typeRegistry); + registerRoutes({ - http: setupDeps.http, + http, + coreUsageData, logger: this.logger, config: this.config, migratorPromise: this.migrator$.pipe(first()).toPromise(), @@ -309,7 +315,7 @@ export class SavedObjectsService return { status$: calculateStatus$( this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())), - setupDeps.elasticsearch.status$ + elasticsearch.status$ ), setClientFactoryProvider: (provider) => { if (this.started) { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2f09ad71de558..f3f4bdfff0e76 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1562,12 +1562,12 @@ export class SavedObjectsRepository { * @param options - {@link SavedObjectsIncrementCounterOptions} * @returns The saved object after the specified fields were incremented */ - async incrementCounter( + async incrementCounter<T = unknown>( type: string, id: string, counterFieldNames: string[], options: SavedObjectsIncrementCounterOptions = {} - ): Promise<SavedObject> { + ): Promise<SavedObject<T>> { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 59f9c4f9ff38c..be654da5660c2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -521,7 +521,7 @@ export interface CoreStatus { } // @internal -export interface CoreUsageData { +export interface CoreUsageData extends CoreUsageStats { // (undocumented) config: CoreConfigUsageData; // (undocumented) @@ -535,6 +535,44 @@ export interface CoreUsageDataStart { getCoreUsageData(): Promise<CoreUsageData>; } +// @internal +export interface CoreUsageStats { + // (undocumented) + 'apiCalls.savedObjectsExport.allTypesSelected.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.total'?: number; +} + // @public (undocumented) export interface CountResponse { // (undocumented) @@ -2448,7 +2486,7 @@ export class SavedObjectsRepository { // (undocumented) find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>; get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>; - incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>; + incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>; update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>; } diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index f377bfc321735..7cc6d108b4cf4 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -185,6 +185,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); expect(mockMetricsService.setup).not.toHaveBeenCalled(); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e253663d8dc8d..0b3249ad58750 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -31,7 +31,7 @@ import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; -import { SavedObjectsService } from './saved_objects'; +import { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects'; import { MetricsService, opsConfig } from './metrics'; import { CapabilitiesService } from './capabilities'; import { EnvironmentService, config as pidConfig } from './environment'; @@ -78,6 +78,9 @@ export class Server { private readonly coreUsageData: CoreUsageDataService; private readonly i18n: I18nService; + private readonly savedObjectsStartPromise: Promise<SavedObjectsServiceStart>; + private resolveSavedObjectsStartPromise?: (value: SavedObjectsServiceStart) => void; + #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; private readonly logger: LoggerFactory; @@ -109,6 +112,10 @@ export class Server { this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); this.i18n = new I18nService(core); + + this.savedObjectsStartPromise = new Promise((resolve) => { + this.resolveSavedObjectsStartPromise = resolve; + }); } public async setup() { @@ -155,9 +162,17 @@ export class Server { http: httpSetup, }); + const metricsSetup = await this.metrics.setup({ http: httpSetup }); + + const coreUsageDataSetup = this.coreUsageData.setup({ + metrics: metricsSetup, + savedObjectsStartPromise: this.savedObjectsStartPromise, + }); + const savedObjectsSetup = await this.savedObjects.setup({ http: httpSetup, elasticsearch: elasticsearchServiceSetup, + coreUsageData: coreUsageDataSetup, }); const uiSettingsSetup = await this.uiSettings.setup({ @@ -165,8 +180,6 @@ export class Server { savedObjects: savedObjectsSetup, }); - const metricsSetup = await this.metrics.setup({ http: httpSetup }); - const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, pluginDependencies: pluginTree.asNames, @@ -191,8 +204,6 @@ export class Server { loggingSystem: this.loggingSystem, }); - this.coreUsageData.setup({ metrics: metricsSetup }); - const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -235,6 +246,8 @@ export class Server { elasticsearch: elasticsearchStart, pluginsInitialized: this.#pluginsInitialized, }); + await this.resolveSavedObjectsStartPromise!(savedObjectsStart); + soStartSpan?.end(); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index a514f9f899e55..d30a3c5ab6861 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -115,6 +115,23 @@ export function getCoreUsageCollector( }, }, }, + 'apiCalls.savedObjectsImport.total': { type: 'long' }, + 'apiCalls.savedObjectsImport.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no': { type: 'long' }, + 'apiCalls.savedObjectsImport.overwriteEnabled.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.overwriteEnabled.no': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.total': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no': { type: 'long' }, + 'apiCalls.savedObjectsExport.total': { type: 'long' }, + 'apiCalls.savedObjectsExport.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsExport.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsExport.allTypesSelected.yes': { type: 'long' }, + 'apiCalls.savedObjectsExport.allTypesSelected.no': { type: 'long' }, }, fetch() { return getCoreUsageDataService().getCoreUsageData(); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 3fbacef99806d..17f15b6aa1c3e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -570,11 +570,17 @@ exports[`Flyout should render import step 1`] = ` hasChildLabel={true} hasEmptyLabelSpace={false} label={ - <FormattedMessage - defaultMessage="Select a file to import" - id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel" - values={Object {}} - /> + <EuiTitle + size="xs" + > + <span> + <FormattedMessage + defaultMessage="Select a file to import" + id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel" + values={Object {}} + /> + </span> + </EuiTitle> } labelType="label" > diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index c19bb5d819158..0ffc162b7ae7a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -758,10 +758,14 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> { <EuiFormRow fullWidth label={ - <FormattedMessage - id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel" - defaultMessage="Select a file to import" - /> + <EuiTitle size="xs"> + <span> + <FormattedMessage + id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel" + defaultMessage="Select a file to import" + /> + </span> + </EuiTitle> } > <EuiFilePicker diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx index 4000d620465a8..acb6aca7181ea 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx @@ -120,7 +120,7 @@ export const ImportModeControl = ({ options={[overwriteEnabled, overwriteDisabled]} idSelected={overwrite ? overwriteEnabled.id : overwriteDisabled.id} onChange={(id: string) => onChange({ overwrite: id === overwriteEnabled.id })} - disabled={createNewCopies} + disabled={createNewCopies && !isLegacyFile} data-test-subj={'savedObjectsManagement-importModeControl-overwriteRadioGroup'} /> ); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index e1078c60caf2e..91039d9ca1c68 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1516,6 +1516,57 @@ } } } + }, + "apiCalls.savedObjectsImport.total": { + "type": "long" + }, + "apiCalls.savedObjectsImport.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsImport.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsImport.createNewCopiesEnabled.yes": { + "type": "long" + }, + "apiCalls.savedObjectsImport.createNewCopiesEnabled.no": { + "type": "long" + }, + "apiCalls.savedObjectsImport.overwriteEnabled.yes": { + "type": "long" + }, + "apiCalls.savedObjectsImport.overwriteEnabled.no": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no": { + "type": "long" + }, + "apiCalls.savedObjectsExport.total": { + "type": "long" + }, + "apiCalls.savedObjectsExport.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsExport.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsExport.allTypesSelected.yes": { + "type": "long" + }, + "apiCalls.savedObjectsExport.allTypesSelected.no": { + "type": "long" } } }, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx index cf406653990c8..3035959f9a941 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { CopyModeControl, CopyModeControlProps } from './copy_mode_control'; describe('CopyModeControl', () => { - const initialValues = { createNewCopies: false, overwrite: true }; // some test cases below make assumptions based on these initial values + const initialValues = { createNewCopies: true, overwrite: true }; // some test cases below make assumptions based on these initial values const updateSelection = jest.fn(); const getOverwriteRadio = (wrapper: ReactWrapper) => @@ -34,21 +34,23 @@ describe('CopyModeControl', () => { const wrapper = mountWithIntl(<CopyModeControl {...props} />); expect(updateSelection).not.toHaveBeenCalled(); - const { createNewCopies } = initialValues; + // need to disable `createNewCopies` first + getCreateNewCopiesDisabled(wrapper).simulate('change'); + const createNewCopies = false; getOverwriteDisabled(wrapper).simulate('change'); - expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies, overwrite: false }); + expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies, overwrite: false }); getOverwriteEnabled(wrapper).simulate('change'); - expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies, overwrite: true }); + expect(updateSelection).toHaveBeenNthCalledWith(3, { createNewCopies, overwrite: true }); }); - it('should disable the Overwrite switch when `createNewCopies` is enabled', async () => { + it('should enable the Overwrite switch when `createNewCopies` is disabled', async () => { const wrapper = mountWithIntl(<CopyModeControl {...props} />); - expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(false); - getCreateNewCopiesEnabled(wrapper).simulate('change'); expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(true); + getCreateNewCopiesDisabled(wrapper).simulate('change'); + expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(false); }); it('should allow the user to toggle `createNewCopies`', async () => { @@ -57,10 +59,10 @@ describe('CopyModeControl', () => { expect(updateSelection).not.toHaveBeenCalled(); const { overwrite } = initialValues; - getCreateNewCopiesEnabled(wrapper).simulate('change'); - expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies: true, overwrite }); - getCreateNewCopiesDisabled(wrapper).simulate('change'); - expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies: false, overwrite }); + expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies: false, overwrite }); + + getCreateNewCopiesEnabled(wrapper).simulate('change'); + expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies: true, overwrite }); }); }); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx index c3e631e335ea7..f060f7e34e230 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx @@ -126,6 +126,15 @@ export const CopyModeControl = ({ initialValues, updateSelection }: CopyModeCont ), }} > + <EuiCheckableCard + id={createNewCopiesEnabled.id} + label={createLabel(createNewCopiesEnabled)} + checked={createNewCopies} + onChange={() => onChange({ createNewCopies: true })} + /> + + <EuiSpacer size="s" /> + <EuiCheckableCard id={createNewCopiesDisabled.id} label={createLabel(createNewCopiesDisabled)} @@ -140,15 +149,6 @@ export const CopyModeControl = ({ initialValues, updateSelection }: CopyModeCont data-test-subj={'cts-copyModeControl-overwriteRadioGroup'} /> </EuiCheckableCard> - - <EuiSpacer size="s" /> - - <EuiCheckableCard - id={createNewCopiesEnabled.id} - label={createLabel(createNewCopiesEnabled)} - checked={createNewCopies} - onChange={() => onChange({ createNewCopies: true })} - /> </EuiFormFieldset> <EuiSpacer size="m" /> diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index ac45db40a3810..96fc3bacd59ba 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -12,6 +12,7 @@ import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; import { Space } from '../../../common/model/space'; import { findTestSubject } from '@kbn/test/jest'; import { SelectableSpacesControl } from './selectable_spaces_control'; +import { CopyModeControl } from './copy_mode_control'; import { act } from '@testing-library/react'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; import { spacesManagerMock } from '../../spaces_manager/mocks'; @@ -289,7 +290,7 @@ describe('CopyToSpaceFlyout', () => { [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }], ['space-1', 'space-2'], true, - false, + true, // `createNewCopies` is enabled by default true ); @@ -376,14 +377,25 @@ describe('CopyToSpaceFlyout', () => { spaceSelector.props().onChange(['space-1', 'space-2']); }); - const startButton = findTestSubject(wrapper, 'cts-initiate-button'); + // Change copy mode to check for conflicts + const copyModeControl = wrapper.find(CopyModeControl); + copyModeControl.find('input[id="createNewCopiesDisabled"]').simulate('change'); await act(async () => { + const startButton = findTestSubject(wrapper, 'cts-initiate-button'); startButton.simulate('click'); await nextTick(); wrapper.update(); }); + expect(mockSpacesManager.copySavedObjects).toHaveBeenCalledWith( + [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }], + ['space-1', 'space-2'], + true, + false, // `createNewCopies` is disabled + true + ); + expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0); expect(wrapper.find(ProcessingCopyToSpace)).toHaveLength(1); @@ -429,7 +441,7 @@ describe('CopyToSpaceFlyout', () => { ], }, true, - false + false // `createNewCopies` is disabled ); expect(onClose).toHaveBeenCalledTimes(1); @@ -545,7 +557,7 @@ describe('CopyToSpaceFlyout', () => { ], }, true, - false + true // `createNewCopies` is enabled by default ); expect(onClose).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 5253eb18bce75..aeb6aab8c8dad 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -42,7 +42,7 @@ interface Props { } const INCLUDE_RELATED_DEFAULT = true; -const CREATE_NEW_COPIES_DEFAULT = false; +const CREATE_NEW_COPIES_DEFAULT = true; const OVERWRITE_ALL_DEFAULT = true; export const CopySavedObjectsToSpaceFlyout = (props: Props) => { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index 551573feebcdb..9c38b747ba074 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiSpacer, EuiFormRow } from '@elastic/eui'; +import { EuiSpacer, EuiTitle, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CopyOptions } from '../types'; import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public'; @@ -45,14 +45,18 @@ export const CopyToSpaceForm = (props: Props) => { updateSelection={(newValues: CopyMode) => changeCopyMode(newValues)} /> - <EuiSpacer /> + <EuiSpacer size="m" /> <EuiFormRow label={ - <FormattedMessage - id="xpack.spaces.management.copyToSpace.selectSpacesLabel" - defaultMessage="Select spaces" - /> + <EuiTitle size="xs"> + <span> + <FormattedMessage + id="xpack.spaces.management.copyToSpace.selectSpacesLabel" + defaultMessage="Select spaces" + /> + </span> + </EuiTitle> } fullWidth > diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index d4e12b31b5b4f..bfd25ba4de0bb 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -72,7 +72,7 @@ export const SelectableSpacesControl = (props: Props) => { className: 'spcCopyToSpace__spacesList', 'data-test-subj': 'cts-form-space-selector', }} - searchable + searchable={options.length > 6} > {(list, search) => { return ( diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx index e53cc152442a2..f6d1576b5067f 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -175,7 +175,7 @@ export const SelectableSpacesControl = (props: Props) => { 'data-test-subj': 'sts-form-space-selector', }} height={ROW_HEIGHT * 3.5} - searchable + searchable={options.length > 6} > {(list, search) => { return ( diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx index bc196208ab35c..75e40b85a37dd 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx @@ -38,7 +38,7 @@ export const ShareToSpaceForm = (props: Props) => { title={ <FormattedMessage id="xpack.spaces.management.shareToSpace.shareWarningTitle" - defaultMessage="Editing a shared object applies the changes in all spaces" + defaultMessage="Editing a shared object applies the changes in every space" /> } color="warning" diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index 8e530ddf8ff2e..856899c127fd2 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -91,7 +91,8 @@ export class SpacesManager { objects, spaces, includeReferences, - ...(createNewCopies ? { createNewCopies } : { overwrite }), + createNewCopies, + ...(createNewCopies ? { overwrite: false } : { overwrite }), // ignore the overwrite option if createNewCopies is enabled }), }); } diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 517fde6ecb41a..cd36ca3c7a6ec 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -20,8 +20,8 @@ import { import { LicensingPluginSetup } from '../../licensing/server'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; import { registerSpacesUsageCollector } from './usage_collection'; -import { SpacesService, SpacesServiceStart } from './spaces_service'; -import { SpacesServiceSetup } from './spaces_service'; +import { SpacesService, SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; +import { UsageStatsService } from './usage_stats'; import { ConfigType } from './config'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; import { initExternalSpacesApi } from './routes/api/external'; @@ -99,6 +99,10 @@ export class Plugin { return this.spacesServiceStart; }; + const usageStatsServicePromise = new UsageStatsService(this.log).setup({ + getStartServices: core.getStartServices, + }); + const savedObjectsService = new SpacesSavedObjectsService(); savedObjectsService.setup({ core, getSpacesService }); @@ -126,6 +130,7 @@ export class Plugin { getStartServices: core.getStartServices, getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit, getSpacesService, + usageStatsServicePromise, }); const internalRouter = core.http.createRouter(); @@ -148,6 +153,7 @@ export class Plugin { kibanaIndexConfig$: this.kibanaIndexConfig$, features: plugins.features, licensing: plugins.licensing, + usageStatsServicePromise, }); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index a6e1c11d011a0..cb81476454cd3 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -22,6 +22,8 @@ import { coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; +import { usageStatsClientMock } from '../../../usage_stats/usage_stats_client.mock'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; @@ -82,6 +84,11 @@ describe('copy to space', () => { basePath: httpService.basePath, }); + const usageStatsClient = usageStatsClientMock.create(); + const usageStatsServicePromise = Promise.resolve( + usageStatsServiceMock.createSetupContract(usageStatsClient) + ); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -95,6 +102,7 @@ describe('copy to space', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); const [ @@ -113,6 +121,7 @@ describe('copy to space', () => { routeHandler: resolveRouteHandler, }, savedObjectsRepositoryMock, + usageStatsClient, }; }; @@ -136,6 +145,27 @@ describe('copy to space', () => { }); }); + it(`records usageStats data`, async () => { + const createNewCopies = Symbol(); + const overwrite = Symbol(); + const payload = { spaces: ['a-space'], objects: [], createNewCopies, overwrite }; + + const { copyToSpace, usageStatsClient } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + body: payload, + method: 'post', + }); + + await copyToSpace.routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(usageStatsClient.incrementCopySavedObjects).toHaveBeenCalledWith({ + headers: request.headers, + createNewCopies, + overwrite, + }); + }); + it(`uses a Saved Objects Client instance without the spaces wrapper`, async () => { const payload = { spaces: ['a-space'], @@ -272,6 +302,25 @@ describe('copy to space', () => { }); }); + it(`records usageStats data`, async () => { + const createNewCopies = Symbol(); + const payload = { retries: {}, objects: [], createNewCopies }; + + const { resolveConflicts, usageStatsClient } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + body: payload, + method: 'post', + }); + + await resolveConflicts.routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(usageStatsClient.incrementResolveCopySavedObjectsErrors).toHaveBeenCalledWith({ + headers: request.headers, + createNewCopies, + }); + }); + it(`uses a Saved Objects Client instance without the spaces wrapper`, async () => { const payload = { retries: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 989c513ac00bc..2b1be42f9cbb0 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -21,7 +21,14 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) => _.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; export function initCopyToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getSpacesService, getImportExportObjectLimit, getStartServices } = deps; + const { + externalRouter, + getSpacesService, + usageStatsServicePromise, + getImportExportObjectLimit, + getStartServices, + } = deps; + const usageStatsClientPromise = usageStatsServicePromise.then(({ getClient }) => getClient()); externalRouter.post( { @@ -63,7 +70,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { ), includeReferences: schema.boolean({ defaultValue: false }), overwrite: schema.boolean({ defaultValue: false }), - createNewCopies: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: true }), }, { validate: (object) => { @@ -77,12 +84,6 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }, createLicensedRouteHandler(async (context, request, response) => { const [startServices] = await getStartServices(); - - const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - startServices.savedObjects, - getImportExportObjectLimit, - request - ); const { spaces: destinationSpaceIds, objects, @@ -90,6 +91,17 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { overwrite, createNewCopies, } = request.body; + + const { headers } = request; + usageStatsClientPromise.then((usageStatsClient) => + usageStatsClient.incrementCopySavedObjects({ headers, createNewCopies, overwrite }) + ); + + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( + startServices.savedObjects, + getImportExportObjectLimit, + request + ); const sourceSpaceId = getSpacesService().getSpaceId(request); const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, { objects, @@ -142,19 +154,24 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { } ), includeReferences: schema.boolean({ defaultValue: false }), - createNewCopies: schema.boolean({ defaultValue: false }), + createNewCopies: schema.boolean({ defaultValue: true }), }), }, }, createLicensedRouteHandler(async (context, request, response) => { const [startServices] = await getStartServices(); + const { objects, includeReferences, retries, createNewCopies } = request.body; + + const { headers } = request; + usageStatsClientPromise.then((usageStatsClient) => + usageStatsClient.incrementResolveCopySavedObjectsErrors({ headers, createNewCopies }) + ); const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( startServices.savedObjects, getImportExportObjectLimit, request ); - const { objects, includeReferences, retries, createNewCopies } = request.body; const sourceSpaceId = getSpacesService().getSpaceId(request); const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts( sourceSpaceId, diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index c9b5fc96094cb..0dc6f67cc278f 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -27,6 +27,7 @@ import { initDeleteSpacesApi } from './delete'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; import { SpacesClientService } from '../../../spaces_client'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -51,6 +52,8 @@ describe('Spaces Public API', () => { basePath: httpService.basePath, }); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -64,6 +67,7 @@ describe('Spaces Public API', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); const [routeDefinition, routeHandler] = router.delete.mock.calls[0]; diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 6fa26a7bcd557..9944655f73b75 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -21,6 +21,7 @@ import { import { SpacesService } from '../../../spaces_service'; import { spacesConfig } from '../../../lib/__fixtures__'; import { SpacesClientService } from '../../../spaces_client'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; describe('GET space', () => { const spacesSavedObjects = createSpaces(); @@ -46,6 +47,8 @@ describe('GET space', () => { basePath: httpService.basePath, }); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -59,6 +62,7 @@ describe('GET space', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); return { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index 5b24a33cb014d..d79596b754fc9 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -22,6 +22,7 @@ import { initGetAllSpacesApi } from './get_all'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; import { SpacesClientService } from '../../../spaces_client'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; describe('GET /spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -47,6 +48,8 @@ describe('GET /spaces/space', () => { basePath: httpService.basePath, }); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -60,6 +63,7 @@ describe('GET /spaces/space', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); return { diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index e34f67adc04ac..b828bb457aba5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -10,7 +10,8 @@ import { initGetSpaceApi } from './get'; import { initGetAllSpacesApi } from './get_all'; import { initPostSpacesApi } from './post'; import { initPutSpacesApi } from './put'; -import { SpacesServiceStart } from '../../../spaces_service/spaces_service'; +import { SpacesServiceStart } from '../../../spaces_service'; +import { UsageStatsServiceSetup } from '../../../usage_stats'; import { initCopyToSpacesApi } from './copy_to_space'; import { initShareToSpacesApi } from './share_to_space'; @@ -19,6 +20,7 @@ export interface ExternalRouteDeps { getStartServices: CoreSetup['getStartServices']; getImportExportObjectLimit: () => number; getSpacesService: () => SpacesServiceStart; + usageStatsServicePromise: Promise<UsageStatsServiceSetup>; log: Logger; } diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index bd8b4f2119109..30429bb2866ef 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -22,6 +22,7 @@ import { initPostSpacesApi } from './post'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; import { SpacesClientService } from '../../../spaces_client'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -46,6 +47,8 @@ describe('Spaces Public API', () => { basePath: httpService.basePath, }); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -59,6 +62,7 @@ describe('Spaces Public API', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); const [routeDefinition, routeHandler] = router.post.mock.calls[0]; diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index d87cfd96e2429..f4aed1efbaa5f 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -23,6 +23,7 @@ import { initPutSpacesApi } from './put'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; import { SpacesClientService } from '../../../spaces_client'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; describe('PUT /api/spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -47,6 +48,8 @@ describe('PUT /api/spaces/space', () => { basePath: httpService.basePath, }); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -60,6 +63,7 @@ describe('PUT /api/spaces/space', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); const [routeDefinition, routeHandler] = router.put.mock.calls[0]; diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts index b376e56a87fd8..9a8a619f66146 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts @@ -23,6 +23,7 @@ import { initShareToSpacesApi } from './share_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; import { SpacesClientService } from '../../../spaces_client'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; describe('share to space', () => { const spacesSavedObjects = createSpaces(); @@ -47,6 +48,8 @@ describe('share to space', () => { basePath: httpService.basePath, }); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + const clientServiceStart = clientService.start(coreStart); const spacesServiceStart = service.start({ @@ -59,6 +62,7 @@ describe('share to space', () => { getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, }); const [ diff --git a/x-pack/plugins/spaces/server/saved_objects/mappings.ts b/x-pack/plugins/spaces/server/saved_objects/mappings.ts index 875a164e25217..7a82e0b667f4a 100644 --- a/x-pack/plugins/spaces/server/saved_objects/mappings.ts +++ b/x-pack/plugins/spaces/server/saved_objects/mappings.ts @@ -38,3 +38,8 @@ export const SpacesSavedObjectMappings = deepFreeze({ }, }, }); + +export const UsageStatsMappings = deepFreeze({ + dynamic: false as false, // we aren't querying or aggregating over this data, so we don't need to specify any fields + properties: {}, +}); diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts index a0b0ab41e9d89..43dccf28c9a8f 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts @@ -5,6 +5,7 @@ */ import { coreMock } from 'src/core/server/mocks'; +import { SPACES_USAGE_STATS_TYPE } from '../usage_stats'; import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; import { SpacesSavedObjectsService } from './saved_objects_service'; @@ -17,51 +18,15 @@ describe('SpacesSavedObjectsService', () => { const service = new SpacesSavedObjectsService(); service.setup({ core, getSpacesService: () => spacesService }); - expect(core.savedObjects.registerType).toHaveBeenCalledTimes(1); - expect(core.savedObjects.registerType.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "hidden": true, - "mappings": Object { - "properties": Object { - "_reserved": Object { - "type": "boolean", - }, - "color": Object { - "type": "keyword", - }, - "description": Object { - "type": "text", - }, - "disabledFeatures": Object { - "type": "keyword", - }, - "imageUrl": Object { - "index": false, - "type": "text", - }, - "initials": Object { - "type": "keyword", - }, - "name": Object { - "fields": Object { - "keyword": Object { - "ignore_above": 2048, - "type": "keyword", - }, - }, - "type": "text", - }, - }, - }, - "migrations": Object { - "6.6.0": [Function], - }, - "name": "space", - "namespaceType": "agnostic", - }, - ] - `); + expect(core.savedObjects.registerType).toHaveBeenCalledTimes(2); + expect(core.savedObjects.registerType).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ name: 'space' }) + ); + expect(core.savedObjects.registerType).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ name: SPACES_USAGE_STATS_TYPE }) + ); }); it('registers the client wrapper', () => { diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts index b52f1eda1b6ac..fa3b36ffbbd57 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts @@ -5,10 +5,11 @@ */ import { CoreSetup } from 'src/core/server'; -import { SpacesSavedObjectMappings } from './mappings'; +import { SpacesSavedObjectMappings, UsageStatsMappings } from './mappings'; import { migrateToKibana660 } from './migrations'; import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory'; import { SpacesServiceStart } from '../spaces_service'; +import { SPACES_USAGE_STATS_TYPE } from '../usage_stats'; interface SetupDeps { core: Pick<CoreSetup, 'savedObjects' | 'getStartServices'>; @@ -27,6 +28,13 @@ export class SpacesSavedObjectsService { }, }); + core.savedObjects.registerType({ + name: SPACES_USAGE_STATS_TYPE, + hidden: true, + namespaceType: 'agnostic', + mappings: UsageStatsMappings, + }); + core.savedObjects.addClientWrapper( Number.MIN_SAFE_INTEGER, 'spaces', diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index 1a377d2f801a0..ea8770b7843cf 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector'; +import { getSpacesUsageCollector, UsageData } from './spaces_usage_collector'; import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; import { KibanaFeature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; +import { UsageStats } from '../usage_stats'; +import { usageStatsClientMock } from '../usage_stats/usage_stats_client.mock'; +import { usageStatsServiceMock } from '../usage_stats/usage_stats_service.mock'; import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; @@ -17,6 +20,21 @@ interface SetupOpts { features?: KibanaFeature[]; } +const MOCK_USAGE_STATS: UsageStats = { + 'apiCalls.copySavedObjects.total': 5, + 'apiCalls.copySavedObjects.kibanaRequest.yes': 5, + 'apiCalls.copySavedObjects.kibanaRequest.no': 0, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': 2, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.no': 3, + 'apiCalls.copySavedObjects.overwriteEnabled.yes': 1, + 'apiCalls.copySavedObjects.overwriteEnabled.no': 4, + 'apiCalls.resolveCopySavedObjectsErrors.total': 13, + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes': 13, + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no': 0, + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': 6, + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': 7, +}; + function setup({ license = { isAvailable: true }, features = [{ id: 'feature1' } as KibanaFeature, { id: 'feature2' } as KibanaFeature], @@ -41,12 +59,18 @@ function setup({ getKibanaFeatures: jest.fn().mockReturnValue(features), } as unknown) as PluginsSetup['features']; + const usageStatsClient = usageStatsClientMock.create(); + usageStatsClient.getUsageStats.mockResolvedValue(MOCK_USAGE_STATS); + const usageStatsService = usageStatsServiceMock.createSetupContract(usageStatsClient); + return { licensing, features: featuresSetup, usageCollection: { makeUsageCollector: (options: any) => new MockUsageCollector(options), }, + usageStatsService, + usageStatsClient, }; } @@ -77,26 +101,28 @@ const getMockFetchContext = (mockedCallCluster: jest.Mock) => { describe('error handling', () => { it('handles a 404 when searching for space usage', async () => { - const { features, licensing, usageCollection } = setup({ + const { features, licensing, usageCollection, usageStatsService } = setup({ license: { isAvailable: true, type: 'basic' }, }); const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, + usageStatsServicePromise: Promise.resolve(usageStatsService), }); await collector.fetch(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 }))); }); it('throws error for a non-404', async () => { - const { features, licensing, usageCollection } = setup({ + const { features, licensing, usageCollection, usageStatsService } = setup({ license: { isAvailable: true, type: 'basic' }, }); const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, + usageStatsServicePromise: Promise.resolve(usageStatsService), }); const statusCodes = [401, 402, 403, 500]; @@ -110,17 +136,19 @@ describe('error handling', () => { }); describe('with a basic license', () => { - let usageStats: UsageStats; + let usageData: UsageData; + const { features, licensing, usageCollection, usageStatsService, usageStatsClient } = setup({ + license: { isAvailable: true, type: 'basic' }, + }); + beforeAll(async () => { - const { features, licensing, usageCollection } = setup({ - license: { isAvailable: true, type: 'basic' }, - }); const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, + usageStatsServicePromise: Promise.resolve(usageStatsService), }); - usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); + usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { body: { @@ -138,87 +166,111 @@ describe('with a basic license', () => { }); test('sets enabled to true', () => { - expect(usageStats.enabled).toBe(true); + expect(usageData.enabled).toBe(true); }); test('sets available to true', () => { - expect(usageStats.available).toBe(true); + expect(usageData.available).toBe(true); }); test('sets the number of spaces', () => { - expect(usageStats.count).toBe(2); + expect(usageData.count).toBe(2); }); test('calculates feature control usage', () => { - expect(usageStats.usesFeatureControls).toBe(true); - expect(usageStats).toHaveProperty('disabledFeatures'); - expect(usageStats.disabledFeatures).toEqual({ + expect(usageData.usesFeatureControls).toBe(true); + expect(usageData).toHaveProperty('disabledFeatures'); + expect(usageData.disabledFeatures).toEqual({ feature1: 1, feature2: 0, }); }); + + test('fetches usageStats data', () => { + expect(usageStatsService.getClient).toHaveBeenCalledTimes(1); + expect(usageStatsClient.getUsageStats).toHaveBeenCalledTimes(1); + expect(usageData).toEqual(expect.objectContaining(MOCK_USAGE_STATS)); + }); }); describe('with no license', () => { - let usageStats: UsageStats; + let usageData: UsageData; + const { features, licensing, usageCollection, usageStatsService, usageStatsClient } = setup({ + license: { isAvailable: false }, + }); + beforeAll(async () => { - const { features, licensing, usageCollection } = setup({ license: { isAvailable: false } }); const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, + usageStatsServicePromise: Promise.resolve(usageStatsService), }); - usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); + usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to false', () => { - expect(usageStats.enabled).toBe(false); + expect(usageData.enabled).toBe(false); }); test('sets available to false', () => { - expect(usageStats.available).toBe(false); + expect(usageData.available).toBe(false); }); test('does not set the number of spaces', () => { - expect(usageStats.count).toBeUndefined(); + expect(usageData.count).toBeUndefined(); }); test('does not set feature control usage', () => { - expect(usageStats.usesFeatureControls).toBeUndefined(); + expect(usageData.usesFeatureControls).toBeUndefined(); + }); + + test('does not fetch usageStats data', () => { + expect(usageStatsService.getClient).not.toHaveBeenCalled(); + expect(usageStatsClient.getUsageStats).not.toHaveBeenCalled(); + expect(usageData).not.toEqual(expect.objectContaining(MOCK_USAGE_STATS)); }); }); describe('with platinum license', () => { - let usageStats: UsageStats; + let usageData: UsageData; + const { features, licensing, usageCollection, usageStatsService, usageStatsClient } = setup({ + license: { isAvailable: true, type: 'platinum' }, + }); + beforeAll(async () => { - const { features, licensing, usageCollection } = setup({ - license: { isAvailable: true, type: 'platinum' }, - }); const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, + usageStatsServicePromise: Promise.resolve(usageStatsService), }); - usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); + usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to true', () => { - expect(usageStats.enabled).toBe(true); + expect(usageData.enabled).toBe(true); }); test('sets available to true', () => { - expect(usageStats.available).toBe(true); + expect(usageData.available).toBe(true); }); test('sets the number of spaces', () => { - expect(usageStats.count).toBe(2); + expect(usageData.count).toBe(2); }); test('calculates feature control usage', () => { - expect(usageStats.usesFeatureControls).toBe(true); - expect(usageStats.disabledFeatures).toEqual({ + expect(usageData.usesFeatureControls).toBe(true); + expect(usageData.disabledFeatures).toEqual({ feature1: 1, feature2: 0, }); }); + + test('fetches usageStats data', () => { + expect(usageStatsService.getClient).toHaveBeenCalledTimes(1); + expect(usageStatsClient.getUsageStats).toHaveBeenCalledTimes(1); + expect(usageData).toEqual(expect.objectContaining(MOCK_USAGE_STATS)); + }); }); diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index d563a4a9b100d..44388453d0707 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -9,6 +9,7 @@ import { take } from 'rxjs/operators'; import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; import { PluginsSetup } from '../plugin'; +import { UsageStats, UsageStatsServiceSetup } from '../usage_stats'; type CallCluster = <T = unknown>( endpoint: string, @@ -33,7 +34,7 @@ interface SpacesAggregationResponse { * @param {string} kibanaIndex * @param {PluginsSetup['features']} features * @param {boolean} spacesAvailable - * @return {UsageStats} + * @return {UsageData} */ async function getSpacesUsage( callCluster: CallCluster, @@ -109,10 +110,22 @@ async function getSpacesUsage( count, usesFeatureControls, disabledFeatures, - } as UsageStats; + } as UsageData; } -export interface UsageStats { +async function getUsageStats( + usageStatsServicePromise: Promise<UsageStatsServiceSetup>, + spacesAvailable: boolean +) { + if (!spacesAvailable) { + return null; + } + + const usageStatsClient = await usageStatsServicePromise.then(({ getClient }) => getClient()); + return usageStatsClient.getUsageStats(); +} + +export interface UsageData extends UsageStats { available: boolean; enabled: boolean; count?: number; @@ -143,6 +156,7 @@ interface CollectorDeps { kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; features: PluginsSetup['features']; licensing: PluginsSetup['licensing']; + usageStatsServicePromise: Promise<UsageStatsServiceSetup>; } /* @@ -153,7 +167,7 @@ export function getSpacesUsageCollector( usageCollection: UsageCollectionSetup, deps: CollectorDeps ) { - return usageCollection.makeUsageCollector<UsageStats>({ + return usageCollection.makeUsageCollector<UsageData>({ type: 'spaces', isReady: () => true, schema: { @@ -181,20 +195,35 @@ export function getSpacesUsageCollector( available: { type: 'boolean' }, enabled: { type: 'boolean' }, count: { type: 'long' }, + 'apiCalls.copySavedObjects.total': { type: 'long' }, + 'apiCalls.copySavedObjects.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.copySavedObjects.kibanaRequest.no': { type: 'long' }, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': { type: 'long' }, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.no': { type: 'long' }, + 'apiCalls.copySavedObjects.overwriteEnabled.yes': { type: 'long' }, + 'apiCalls.copySavedObjects.overwriteEnabled.no': { type: 'long' }, + 'apiCalls.resolveCopySavedObjectsErrors.total': { type: 'long' }, + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no': { type: 'long' }, + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': { type: 'long' }, + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': { type: 'long' }, }, fetch: async ({ callCluster }: CollectorFetchContext) => { - const license = await deps.licensing.license$.pipe(take(1)).toPromise(); + const { licensing, kibanaIndexConfig$, features, usageStatsServicePromise } = deps; + const license = await licensing.license$.pipe(take(1)).toPromise(); const available = license.isAvailable; // some form of spaces is available for all valid licenses - const kibanaIndex = (await deps.kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index; + const kibanaIndex = (await kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index; - const usageStats = await getSpacesUsage(callCluster, kibanaIndex, deps.features, available); + const usageData = await getSpacesUsage(callCluster, kibanaIndex, features, available); + const usageStats = await getUsageStats(usageStatsServicePromise, available); return { available, enabled: available, + ...usageData, ...usageStats, - } as UsageStats; + } as UsageData; }, }); } diff --git a/x-pack/plugins/spaces/server/usage_stats/constants.ts b/x-pack/plugins/spaces/server/usage_stats/constants.ts new file mode 100644 index 0000000000000..60fc98d868e4d --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SPACES_USAGE_STATS_TYPE = 'spaces-usage-stats'; +export const SPACES_USAGE_STATS_ID = 'spaces-usage-stats'; diff --git a/x-pack/plugins/spaces/server/usage_stats/index.ts b/x-pack/plugins/spaces/server/usage_stats/index.ts new file mode 100644 index 0000000000000..f661a39934608 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SPACES_USAGE_STATS_TYPE } from './constants'; +export { UsageStatsService, UsageStatsServiceSetup } from './usage_stats_service'; +export { UsageStats } from './types'; diff --git a/x-pack/plugins/spaces/server/usage_stats/types.ts b/x-pack/plugins/spaces/server/usage_stats/types.ts new file mode 100644 index 0000000000000..05733d6bf3a11 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UsageStats { + 'apiCalls.copySavedObjects.total'?: number; + 'apiCalls.copySavedObjects.kibanaRequest.yes'?: number; + 'apiCalls.copySavedObjects.kibanaRequest.no'?: number; + 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes'?: number; + 'apiCalls.copySavedObjects.createNewCopiesEnabled.no'?: number; + 'apiCalls.copySavedObjects.overwriteEnabled.yes'?: number; + 'apiCalls.copySavedObjects.overwriteEnabled.no'?: number; + 'apiCalls.resolveCopySavedObjectsErrors.total'?: number; + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes'?: number; + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no'?: number; + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes'?: number; + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no'?: number; +} diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.mock.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.mock.ts new file mode 100644 index 0000000000000..f1b17430a7655 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageStatsClient } from './usage_stats_client'; + +const createUsageStatsClientMock = () => + (({ + getUsageStats: jest.fn().mockResolvedValue({}), + incrementCopySavedObjects: jest.fn().mockResolvedValue(null), + incrementResolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(null), + } as unknown) as jest.Mocked<UsageStatsClient>); + +export const usageStatsClientMock = { + create: createUsageStatsClientMock, +}; diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts new file mode 100644 index 0000000000000..b313c0be32b95 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { SPACES_USAGE_STATS_TYPE, SPACES_USAGE_STATS_ID } from './constants'; +import { + UsageStatsClient, + IncrementCopySavedObjectsOptions, + IncrementResolveCopySavedObjectsErrorsOptions, + COPY_STATS_PREFIX, + RESOLVE_COPY_STATS_PREFIX, +} from './usage_stats_client'; + +describe('UsageStatsClient', () => { + const setup = () => { + const debugLoggerMock = jest.fn(); + const repositoryMock = savedObjectsRepositoryMock.create(); + const usageStatsClient = new UsageStatsClient(debugLoggerMock, Promise.resolve(repositoryMock)); + return { usageStatsClient, debugLoggerMock, repositoryMock }; + }; + + const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request + const incrementOptions = { refresh: false }; + + describe('#getUsageStats', () => { + it('calls repository.incrementCounter and initializes fields', async () => { + const { usageStatsClient, repositoryMock } = setup(); + await usageStatsClient.getUsageStats(); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + [ + `${COPY_STATS_PREFIX}.total`, + `${COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${COPY_STATS_PREFIX}.kibanaRequest.no`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${COPY_STATS_PREFIX}.overwriteEnabled.yes`, + `${COPY_STATS_PREFIX}.overwriteEnabled.no`, + `${RESOLVE_COPY_STATS_PREFIX}.total`, + `${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, + ], + { initialize: true } + ); + }); + + it('returns empty object when encountering a repository error', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const result = await usageStatsClient.getUsageStats(); + expect(result).toEqual({}); + }); + + it('returns object attributes when usageStats data exists', async () => { + const { usageStatsClient, repositoryMock } = setup(); + const usageStats = { foo: 'bar' }; + repositoryMock.incrementCounter.mockResolvedValue({ + type: SPACES_USAGE_STATS_TYPE, + id: SPACES_USAGE_STATS_ID, + attributes: usageStats, + references: [], + }); + + const result = await usageStatsClient.getUsageStats(); + expect(result).toEqual(usageStats); + }); + }); + + describe('#incrementCopySavedObjects', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + await expect( + usageStatsClient.incrementCopySavedObjects({} as IncrementCopySavedObjectsOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementCopySavedObjects({} as IncrementCopySavedObjectsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + [ + `${COPY_STATS_PREFIX}.total`, + `${COPY_STATS_PREFIX}.kibanaRequest.no`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${COPY_STATS_PREFIX}.overwriteEnabled.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementCopySavedObjects({ + headers: firstPartyRequestHeaders, + createNewCopies: true, + overwrite: true, + } as IncrementCopySavedObjectsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + [ + `${COPY_STATS_PREFIX}.total`, + `${COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${COPY_STATS_PREFIX}.overwriteEnabled.yes`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementResolveCopySavedObjectsErrors', () => { + it('does not throw an error if repository create operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + await expect( + usageStatsClient.incrementResolveCopySavedObjectsErrors( + {} as IncrementResolveCopySavedObjectsErrorsOptions + ) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementResolveCopySavedObjectsErrors( + {} as IncrementResolveCopySavedObjectsErrorsOptions + ); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + [ + `${RESOLVE_COPY_STATS_PREFIX}.total`, + `${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + await usageStatsClient.incrementResolveCopySavedObjectsErrors({ + headers: firstPartyRequestHeaders, + createNewCopies: true, + } as IncrementResolveCopySavedObjectsErrorsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + [ + `${RESOLVE_COPY_STATS_PREFIX}.total`, + `${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + ], + incrementOptions + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts new file mode 100644 index 0000000000000..4c9d11a11ccca --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ISavedObjectsRepository, Headers } from 'src/core/server'; +import { SPACES_USAGE_STATS_TYPE, SPACES_USAGE_STATS_ID } from './constants'; +import { CopyOptions, ResolveConflictsOptions } from '../lib/copy_to_spaces/types'; +import { UsageStats } from './types'; + +interface BaseIncrementOptions { + headers?: Headers; +} +export type IncrementCopySavedObjectsOptions = BaseIncrementOptions & + Pick<CopyOptions, 'createNewCopies' | 'overwrite'>; +export type IncrementResolveCopySavedObjectsErrorsOptions = BaseIncrementOptions & + Pick<ResolveConflictsOptions, 'createNewCopies'>; + +export const COPY_STATS_PREFIX = 'apiCalls.copySavedObjects'; +export const RESOLVE_COPY_STATS_PREFIX = 'apiCalls.resolveCopySavedObjectsErrors'; +const ALL_COUNTER_FIELDS = [ + `${COPY_STATS_PREFIX}.total`, + `${COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${COPY_STATS_PREFIX}.kibanaRequest.no`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${COPY_STATS_PREFIX}.overwriteEnabled.yes`, + `${COPY_STATS_PREFIX}.overwriteEnabled.no`, + `${RESOLVE_COPY_STATS_PREFIX}.total`, + `${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`, + `${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.no`, +]; +export class UsageStatsClient { + constructor( + private readonly debugLogger: (message: string) => void, + private readonly repositoryPromise: Promise<ISavedObjectsRepository> + ) {} + + public async getUsageStats() { + this.debugLogger('getUsageStats() called'); + let usageStats: UsageStats = {}; + try { + const repository = await this.repositoryPromise; + const result = await repository.incrementCounter<UsageStats>( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + ALL_COUNTER_FIELDS, + { initialize: true } + ); + usageStats = result.attributes; + } catch (err) { + // do nothing + } + return usageStats; + } + + public async incrementCopySavedObjects({ + headers, + createNewCopies, + overwrite, + }: IncrementCopySavedObjectsOptions) { + const isKibanaRequest = getIsKibanaRequest(headers); + const counterFieldNames = [ + 'total', + `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, + `overwriteEnabled.${overwrite ? 'yes' : 'no'}`, + ]; + await this.updateUsageStats(counterFieldNames, COPY_STATS_PREFIX); + } + + public async incrementResolveCopySavedObjectsErrors({ + headers, + createNewCopies, + }: IncrementResolveCopySavedObjectsErrorsOptions) { + const isKibanaRequest = getIsKibanaRequest(headers); + const counterFieldNames = [ + 'total', + `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, + ]; + await this.updateUsageStats(counterFieldNames, RESOLVE_COPY_STATS_PREFIX); + } + + private async updateUsageStats(counterFieldNames: string[], prefix: string) { + const options = { refresh: false }; + try { + const repository = await this.repositoryPromise; + await repository.incrementCounter( + SPACES_USAGE_STATS_TYPE, + SPACES_USAGE_STATS_ID, + counterFieldNames.map((x) => `${prefix}.${x}`), + options + ); + } catch (err) { + // do nothing + } + } +} + +function getIsKibanaRequest(headers?: Headers) { + // The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client. + // We can't be 100% certain, but this is a reasonable attempt. + return headers && headers['kbn-version'] && headers.origin && headers.referer; +} diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.mock.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.mock.ts new file mode 100644 index 0000000000000..337d6144bd99d --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.mock.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { usageStatsClientMock } from './usage_stats_client.mock'; +import { UsageStatsServiceSetup } from './usage_stats_service'; + +const createSetupContractMock = (usageStatsClient = usageStatsClientMock.create()) => { + const setupContract: jest.Mocked<UsageStatsServiceSetup> = { + getClient: jest.fn().mockReturnValue(usageStatsClient), + }; + return setupContract; +}; + +export const usageStatsServiceMock = { + createSetupContract: createSetupContractMock, +}; diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.test.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.test.ts new file mode 100644 index 0000000000000..5695a39414155 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; +import { UsageStatsService } from '.'; +import { UsageStatsClient } from './usage_stats_client'; +import { SPACES_USAGE_STATS_TYPE } from './constants'; + +describe('UsageStatsService', () => { + const mockLogger = loggingSystemMock.createLogger(); + + describe('#setup', () => { + const setup = async () => { + const core = coreMock.createSetup(); + const usageStatsService = await new UsageStatsService(mockLogger).setup(core); + return { core, usageStatsService }; + }; + + it('creates internal repository', async () => { + const { core } = await setup(); + + const [{ savedObjects }] = await core.getStartServices(); + expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1); + expect(savedObjects.createInternalRepository).toHaveBeenCalledWith([SPACES_USAGE_STATS_TYPE]); + }); + + describe('#getClient', () => { + it('returns client', async () => { + const { usageStatsService } = await setup(); + + const usageStatsClient = usageStatsService.getClient(); + expect(usageStatsClient).toBeInstanceOf(UsageStatsClient); + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.ts new file mode 100644 index 0000000000000..e6a01bdddfd69 --- /dev/null +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_service.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger, CoreSetup } from '../../../../../src/core/server'; +import { UsageStatsClient } from './usage_stats_client'; +import { SPACES_USAGE_STATS_TYPE } from './constants'; + +export interface UsageStatsServiceSetup { + getClient(): UsageStatsClient; +} + +interface UsageStatsServiceDeps { + getStartServices: CoreSetup['getStartServices']; +} + +export class UsageStatsService { + constructor(private readonly log: Logger) {} + + public async setup({ getStartServices }: UsageStatsServiceDeps): Promise<UsageStatsServiceSetup> { + const internalRepositoryPromise = getStartServices().then(([coreStart]) => + coreStart.savedObjects.createInternalRepository([SPACES_USAGE_STATS_TYPE]) + ); + + const getClient = () => { + const debugLogger = (message: string) => this.log.debug(message); + return new UsageStatsClient(debugLogger, internalRepositoryPromise); + }; + + return { getClient }; + } + + public async stop() {} +} diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index e1b5f4cb9c3ae..f4eb00644b4ec 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3381,6 +3381,42 @@ }, "count": { "type": "long" + }, + "apiCalls.copySavedObjects.total": { + "type": "long" + }, + "apiCalls.copySavedObjects.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.copySavedObjects.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.copySavedObjects.createNewCopiesEnabled.yes": { + "type": "long" + }, + "apiCalls.copySavedObjects.createNewCopiesEnabled.no": { + "type": "long" + }, + "apiCalls.copySavedObjects.overwriteEnabled.yes": { + "type": "long" + }, + "apiCalls.copySavedObjects.overwriteEnabled.no": { + "type": "long" + }, + "apiCalls.resolveCopySavedObjectsErrors.total": { + "type": "long" + }, + "apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes": { + "type": "long" + }, + "apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no": { + "type": "long" } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce193db011544..0f4bec9ac021b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19330,7 +19330,6 @@ "xpack.spaces.management.shareToSpace.shareToSpacesButton": "保存して閉じる", "xpack.spaces.management.shareToSpace.shareWarningBody": "1つのスペースでのみ編集するには、{makeACopyLink}してください。", "xpack.spaces.management.shareToSpace.shareWarningLink": "コピーを作成", - "xpack.spaces.management.shareToSpace.shareWarningTitle": "共有オブジェクトの編集は、すべてのスペースで変更を適用します。", "xpack.spaces.management.shareToSpace.showLessSpacesLink": "縮小表示", "xpack.spaces.management.shareToSpace.showMoreSpacesLink": "他{count}件", "xpack.spaces.management.shareToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3414350044f17..33163a1f337ee 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19349,7 +19349,6 @@ "xpack.spaces.management.shareToSpace.shareToSpacesButton": "保存并关闭", "xpack.spaces.management.shareToSpace.shareWarningBody": "要仅在一个工作区中编辑,请改为{makeACopyLink}。", "xpack.spaces.management.shareToSpace.shareWarningLink": "创建副本", - "xpack.spaces.management.shareToSpace.shareWarningTitle": "编辑共享对象会在所有工作区中应用更改", "xpack.spaces.management.shareToSpace.showLessSpacesLink": "显示更少", "xpack.spaces.management.shareToSpace.showMoreSpacesLink": "另外 {count} 个", "xpack.spaces.management.shareToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 8f29ae6a27c3a..b14424154a04e 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -52,6 +52,7 @@ export default function spaceSelectorFunctonalTests({ await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard'); await PageObjects.copySavedObjectsToSpace.setupForm({ + createNewCopies: false, overwrite: true, destinationSpaceId, }); @@ -80,6 +81,7 @@ export default function spaceSelectorFunctonalTests({ await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard'); await PageObjects.copySavedObjectsToSpace.setupForm({ + createNewCopies: false, overwrite: false, destinationSpaceId, }); @@ -116,12 +118,42 @@ export default function spaceSelectorFunctonalTests({ await PageObjects.copySavedObjectsToSpace.finishCopy(); }); + it('avoids conflicts when createNewCopies is enabled', async () => { + const destinationSpaceId = 'sales'; + + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard'); + + await PageObjects.copySavedObjectsToSpace.setupForm({ + createNewCopies: true, + overwrite: false, + destinationSpaceId, + }); + + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 3, + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + }); + it('allows a dashboard to be copied to the marketing space, with circular references', async () => { const destinationSpaceId = 'marketing'; await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('Dashboard Foo'); await PageObjects.copySavedObjectsToSpace.setupForm({ + createNewCopies: false, overwrite: true, destinationSpaceId, }); diff --git a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts index 00a364bb7543e..e77c33b69dcdb 100644 --- a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts +++ b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts @@ -28,12 +28,24 @@ export function CopySavedObjectsToSpacePageProvider({ }, async setupForm({ + createNewCopies, overwrite, destinationSpaceId, }: { + createNewCopies?: boolean; overwrite?: boolean; destinationSpaceId: string; }) { + if (createNewCopies && overwrite) { + throw new Error('createNewCopies and overwrite options cannot be used together'); + } + if (!createNewCopies) { + const form = await testSubjects.find('copy-to-space-form'); + // a radio button consists of a div tag that contains an input, a div, and a label + // we can't click the input directly, need to click the label + const label = await form.findByCssSelector('label[for="createNewCopiesDisabled"]'); + await label.click(); + } if (!overwrite) { const radio = await testSubjects.find('cts-copyModeControl-overwriteRadioGroup'); // a radio button consists of a div tag that contains an input, a div, and a label diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 2039134f68bbc..24fa3e642a832 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -607,6 +607,7 @@ export function copyToSpaceTestSuiteFactory( objects: [dashboardObject], spaces: [destination], includeReferences: false, + createNewCopies: false, overwrite: false, }) .expect(tests.noConflictsWithoutReferences.statusCode) @@ -625,6 +626,7 @@ export function copyToSpaceTestSuiteFactory( objects: [dashboardObject], spaces: [destination], includeReferences: true, + createNewCopies: false, overwrite: false, }) .expect(tests.noConflictsWithReferences.statusCode) @@ -643,6 +645,7 @@ export function copyToSpaceTestSuiteFactory( objects: [dashboardObject], spaces: [destination], includeReferences: true, + createNewCopies: false, overwrite: true, }) .expect(tests.withConflictsOverwriting.statusCode) @@ -661,6 +664,7 @@ export function copyToSpaceTestSuiteFactory( objects: [dashboardObject], spaces: [destination], includeReferences: true, + createNewCopies: false, overwrite: false, }) .expect(tests.withConflictsWithoutOverwriting.statusCode) @@ -678,6 +682,7 @@ export function copyToSpaceTestSuiteFactory( objects: [dashboardObject], spaces: [conflictDestination, noConflictDestination], includeReferences: true, + createNewCopies: false, overwrite: true, }) .expect(tests.multipleSpaces.statusCode) @@ -710,6 +715,7 @@ export function copyToSpaceTestSuiteFactory( objects: [dashboardObject], spaces: ['non_existent_space'], includeReferences: false, + createNewCopies: false, overwrite: true, }) .expect(tests.nonExistentSpace.statusCode) @@ -720,6 +726,7 @@ export function copyToSpaceTestSuiteFactory( [false, true].forEach((overwrite) => { const spaces = ['space_2']; const includeReferences = false; + const createNewCopies = false; describe(`multi-namespace types with overwrite=${overwrite}`, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); @@ -730,7 +737,7 @@ export function copyToSpaceTestSuiteFactory( return supertest .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`) .auth(user.username, user.password) - .send({ objects, spaces, includeReferences, overwrite }) + .send({ objects, spaces, includeReferences, createNewCopies, overwrite }) .expect(statusCode) .then(response); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 63f5de1976440..1ae7c7acd6655 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -442,6 +442,7 @@ export function resolveCopyToSpaceConflictsSuite( .send({ objects: [dashboardObject], includeReferences: true, + createNewCopies: false, retries: { [destination]: [{ ...visualizationObject, overwrite: false }] }, }) .expect(tests.withReferencesNotOverwriting.statusCode) @@ -457,6 +458,7 @@ export function resolveCopyToSpaceConflictsSuite( .send({ objects: [dashboardObject], includeReferences: true, + createNewCopies: false, retries: { [destination]: [{ ...visualizationObject, overwrite: true }] }, }) .expect(tests.withReferencesOverwriting.statusCode) @@ -472,6 +474,7 @@ export function resolveCopyToSpaceConflictsSuite( .send({ objects: [dashboardObject], includeReferences: false, + createNewCopies: false, retries: { [destination]: [{ ...dashboardObject, overwrite: true }] }, }) .expect(tests.withoutReferencesOverwriting.statusCode) @@ -487,6 +490,7 @@ export function resolveCopyToSpaceConflictsSuite( .send({ objects: [dashboardObject], includeReferences: false, + createNewCopies: false, retries: { [destination]: [{ ...dashboardObject, overwrite: false }] }, }) .expect(tests.withoutReferencesNotOverwriting.statusCode) @@ -502,6 +506,7 @@ export function resolveCopyToSpaceConflictsSuite( .send({ objects: [dashboardObject], includeReferences: false, + createNewCopies: false, retries: { [destination]: [{ ...dashboardObject, overwrite: true }] }, }) .expect(tests.nonExistentSpace.statusCode) @@ -510,6 +515,7 @@ export function resolveCopyToSpaceConflictsSuite( }); const includeReferences = false; + const createNewCopies = false; describe(`multi-namespace types with "overwrite" retry`, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); @@ -520,7 +526,7 @@ export function resolveCopyToSpaceConflictsSuite( return supertestWithoutAuth .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`) .auth(user.username, user.password) - .send({ objects, includeReferences, retries }) + .send({ objects, includeReferences, createNewCopies, retries }) .expect(statusCode) .then(response); }); From 235f786c3e03a5e8e7c5c77abb755c4192f0f95b Mon Sep 17 00:00:00 2001 From: Scotty Bollinger <scotty.bollinger@elastic.co> Date: Thu, 3 Dec 2020 10:13:22 -0600 Subject: [PATCH 107/107] [Workplace Search] Migrate Sources Schema tree (#84847) * Initial copy/paste of component tree Only does linting changes and: - lodash imports - Replace unescaped apostrophes with ' - Fix ternary function call to if block: if (isAdding) { actions.onSchemaSetFormErrors(errors); } else { actions.onSchemaSetError({ flashMessages: { error: errors } }); } * Remove local flash messages from component * Update paths - Adds getReindexJobRoute method to routes - Repalces legacy Rails routes helper with hard-coded paths * Add types and constants * Update paths * Replace local flash message logic with gobal * Update with newly added types Added here: https://github.com/elastic/kibana/pull/84822 * Update server routes * Replace Rails http with kibana http * Set percentage to 0 when updating Without this, the IndexingStatus never shows. * Fix route paths * Fix server route validation The empty object was breaking the UI since `schema.object({})` is actually an empty object. This is more explicit and correct. * Add i18n * Make sure i18n key is unique * Lint --- .../shared/constants/operations.ts | 9 + .../public/applications/shared/types.ts | 9 + .../applications/workplace_search/routes.ts | 6 + .../components/schema/constants.ts | 105 ++++++ .../components/schema/schema.tsx | 159 +++++++- .../schema/schema_change_errors.tsx | 40 +- .../components/schema/schema_fields_table.tsx | 77 ++++ .../components/schema/schema_logic.ts | 357 ++++++++++++++++++ .../server/routes/workplace_search/sources.ts | 14 +- 9 files changed, 770 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts new file mode 100644 index 0000000000000..96043bb4046ed --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/operations.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADD = 'add'; +export const UPDATE = 'update'; +export const REMOVE = 'remove'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 38a6187d290b5..c1737142e482e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ADD, UPDATE } from './constants/operations'; + export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; export interface Schema { @@ -32,3 +34,10 @@ export interface IIndexingStatus { numDocumentsWithErrors: number; activeReindexJobId: number; } + +export interface IndexJob extends IIndexingStatus { + isActive?: boolean; + hasErrors?: boolean; +} + +export type TOperation = typeof ADD | typeof UPDATE; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 14c288de5a0c8..868d76f7d09c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -126,3 +126,9 @@ export const getGroupSourcePrioritizationPath = (groupId: string): string => `${GROUPS_PATH}/${groupId}/source_prioritization`; export const getSourcesPath = (path: string, isOrganization: boolean): string => isOrganization ? path : `${PERSONAL_PATH}${path}`; +export const getReindexJobRoute = ( + sourceId: string, + activeReindexJobId: string, + isOrganization: boolean +) => + getSourcesPath(generatePath(REINDEX_JOB_PATH, { sourceId, activeReindexJobId }), isOrganization); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts new file mode 100644 index 0000000000000..104331dcd97bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/constants.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const SCHEMA_ERRORS_HEADING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.heading', + { + defaultMessage: 'Schema Change Errors', + } +); + +export const SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.header.fieldName', + { + defaultMessage: 'Field Name', + } +); + +export const SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.header.dataType', + { + defaultMessage: 'Data Type', + } +); + +export const SCHEMA_FIELD_ERRORS_ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.errors.message', + { + defaultMessage: 'Oops, we were not able to find any errors for this Schema.', + } +); + +export const SCHEMA_FIELD_ADDED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.fieldAdded.message', + { + defaultMessage: 'New field added.', + } +); + +export const SCHEMA_UPDATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.updated.message', + { + defaultMessage: 'Schema updated.', + } +); + +export const SCHEMA_ADD_FIELD_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.addField.button', + { + defaultMessage: 'Add field', + } +); + +export const SCHEMA_MANAGE_SCHEMA_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.manage.title', + { + defaultMessage: 'Manage source schema', + } +); + +export const SCHEMA_MANAGE_SCHEMA_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.manage.description', + { + defaultMessage: 'Add new fields or change the types of existing ones', + } +); + +export const SCHEMA_FILTER_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.filter.placeholder', + { + defaultMessage: 'Filter schema fields...', + } +); + +export const SCHEMA_UPDATING = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.updating', + { + defaultMessage: 'Updating schema...', + } +); + +export const SCHEMA_SAVE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.save.button', + { + defaultMessage: 'Save schema', + } +); + +export const SCHEMA_EMPTY_SCHEMA_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.title', + { + defaultMessage: 'Content source does not have a schema', + } +); + +export const SCHEMA_EMPTY_SCHEMA_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.description', + { + defaultMessage: + 'A schema is created for you once you index some documents. Click below to create schema fields in advance.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index 55f1e1e03b2db..6a1991e4c39e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -4,6 +4,161 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; -export const Schema: React.FC = () => <>Schema Placeholder</>; +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; + +import { getReindexJobRoute } from '../../../../routes'; +import { AppLogic } from '../../../../app_logic'; + +import { Loading } from '../../../../../shared/loading'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; + +import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; +import { IndexingStatus } from '../../../../../shared/indexing_status'; + +import { SchemaFieldsTable } from './schema_fields_table'; +import { SchemaLogic } from './schema_logic'; + +import { + SCHEMA_ADD_FIELD_BUTTON, + SCHEMA_MANAGE_SCHEMA_TITLE, + SCHEMA_MANAGE_SCHEMA_DESCRIPTION, + SCHEMA_FILTER_PLACEHOLDER, + SCHEMA_UPDATING, + SCHEMA_SAVE_BUTTON, + SCHEMA_EMPTY_SCHEMA_TITLE, + SCHEMA_EMPTY_SCHEMA_DESCRIPTION, +} from './constants'; + +export const Schema: React.FC = () => { + const { + initializeSchema, + onIndexingComplete, + addNewField, + updateFields, + openAddFieldModal, + closeAddFieldModal, + setFilterValue, + } = useActions(SchemaLogic); + + const { + sourceId, + activeSchema, + filterValue, + showAddFieldModal, + addFieldFormErrors, + mostRecentIndexJob, + formUnchanged, + dataLoading, + } = useValues(SchemaLogic); + + const { isOrganization } = useValues(AppLogic); + + useEffect(() => { + initializeSchema(); + }, []); + + if (dataLoading) return <Loading />; + + const hasSchemaFields = Object.keys(activeSchema).length > 0; + const { isActive, hasErrors, percentageComplete, activeReindexJobId } = mostRecentIndexJob; + + const addFieldButton = ( + <EuiButtonEmpty color="primary" data-test-subj="AddFieldButton" onClick={openAddFieldModal}> + {SCHEMA_ADD_FIELD_BUTTON} + </EuiButtonEmpty> + ); + const statusPath = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/reindex_job/${activeReindexJobId}/status` + : `/api/workplace_search/account/sources/${sourceId}/reindex_job/${activeReindexJobId}/status`; + + return ( + <> + <ViewContentHeader + title={SCHEMA_MANAGE_SCHEMA_TITLE} + description={SCHEMA_MANAGE_SCHEMA_DESCRIPTION} + /> + <div> + {(isActive || hasErrors) && ( + <IndexingStatus + itemId={sourceId} + viewLinkPath={getReindexJobRoute( + sourceId, + mostRecentIndexJob.activeReindexJobId.toString(), + isOrganization + )} + statusPath={statusPath} + onComplete={onIndexingComplete} + {...mostRecentIndexJob} + /> + )} + {hasSchemaFields ? ( + <> + <EuiSpacer /> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiFieldSearch + value={filterValue} + data-test-subj="FilterSchemaInput" + placeholder={SCHEMA_FILTER_PLACEHOLDER} + onChange={(e) => setFilterValue(e.target.value)} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem>{addFieldButton}</EuiFlexItem> + <EuiFlexItem grow={false}> + {percentageComplete < 100 ? ( + <EuiButton isLoading={true} fill={true}> + {SCHEMA_UPDATING} + </EuiButton> + ) : ( + <EuiButton + disabled={formUnchanged} + data-test-subj="UpdateTypesButton" + onClick={updateFields} + fill={true} + > + {SCHEMA_SAVE_BUTTON} + </EuiButton> + )} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <SchemaFieldsTable /> + </> + ) : ( + <EuiPanel className="euiPanel--inset"> + <EuiEmptyPrompt + iconType="managementApp" + title={<h2>{SCHEMA_EMPTY_SCHEMA_TITLE}</h2>} + body={<p>{SCHEMA_EMPTY_SCHEMA_DESCRIPTION}</p>} + actions={addFieldButton} + /> + </EuiPanel> + )} + </div> + {showAddFieldModal && ( + <SchemaAddFieldModal + addFieldFormErrors={addFieldFormErrors} + addNewField={addNewField} + closeAddFieldModal={closeAddFieldModal} + /> + )} + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index dd772b86a00e2..7fc923875dcdf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -4,6 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; -export const SchemaChangeErrors: React.FC = () => <>Schema Errors Placeholder</>; +import { useActions, useValues } from 'kea'; + +import { EuiSpacer } from '@elastic/eui'; + +import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { SchemaLogic } from './schema_logic'; +import { SCHEMA_ERRORS_HEADING } from './constants'; + +export const SchemaChangeErrors: React.FC = () => { + const { activeReindexJobId, sourceId } = useParams() as { + activeReindexJobId: string; + sourceId: string; + }; + const { initializeSchemaFieldErrors } = useActions(SchemaLogic); + + const { fieldCoercionErrors, serverSchema } = useValues(SchemaLogic); + + useEffect(() => { + initializeSchemaFieldErrors(activeReindexJobId, sourceId); + }, []); + + return ( + <div> + <ViewContentHeader title={SCHEMA_ERRORS_HEADING} /> + <EuiSpacer size="xl" /> + <main> + <SchemaErrorsAccordion + fieldCoercionErrors={fieldCoercionErrors} + schema={serverSchema} + itemId={sourceId} + /> + </main> + </div> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx new file mode 100644 index 0000000000000..b1eac0a3d8734 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, +} from '@elastic/eui'; + +import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; +import { SchemaLogic } from './schema_logic'; +import { + SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER, + SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER, +} from './constants'; + +export const SchemaFieldsTable: React.FC = () => { + const { updateExistingFieldType } = useActions(SchemaLogic); + + const { filteredSchemaFields, filterValue } = useValues(SchemaLogic); + + return Object.keys(filteredSchemaFields).length > 0 ? ( + <EuiTable> + <EuiTableHeader> + <EuiTableHeaderCell>{SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER}</EuiTableHeaderCell> + <EuiTableHeaderCell>{SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER}</EuiTableHeaderCell> + </EuiTableHeader> + <EuiTableBody> + {Object.keys(filteredSchemaFields).map((fieldName) => ( + <EuiTableRow key={fieldName} data-test-subj="SchemaFieldRow"> + <EuiTableRowCell> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem> + <strong>{fieldName}</strong> + </EuiFlexItem> + </EuiFlexGroup> + </EuiTableRowCell> + <EuiTableRowCell> + <SchemaExistingField + disabled={fieldName === 'id'} + key={fieldName} + fieldName={fieldName} + hideName={true} + fieldType={filteredSchemaFields[fieldName]} + updateExistingFieldType={updateExistingFieldType} + /> + </EuiTableRowCell> + </EuiTableRow> + ))} + </EuiTableBody> + </EuiTable> + ) : ( + <p> + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.filter.noResults.message', + { + defaultMessage: 'No results found for "{filterValue}".', + values: { filterValue }, + } + )} + </p> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts new file mode 100644 index 0000000000000..36eb3fc67b2c2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -0,0 +1,357 @@ +/* + * 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 { cloneDeep, isEqual } from 'lodash'; +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../../../../../shared/http'; + +import { TEXT } from '../../../../../shared/constants/field_types'; +import { ADD, UPDATE } from '../../../../../shared/constants/operations'; +import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; +import { OptionValue } from '../../../../types'; + +import { + flashAPIErrors, + setSuccessMessage, + FlashMessagesLogic, +} from '../../../../../shared/flash_messages'; + +import { AppLogic } from '../../../../app_logic'; +import { SourceLogic } from '../../source_logic'; + +import { + SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, + SCHEMA_FIELD_ADDED_MESSAGE, + SCHEMA_UPDATED_MESSAGE, +} from './constants'; + +interface SchemaActions { + onInitializeSchema(schemaProps: SchemaInitialData): SchemaInitialData; + onInitializeSchemaFieldErrors( + fieldCoercionErrorsProps: SchemaChangeErrorsProps + ): SchemaChangeErrorsProps; + onSchemaSetSuccess(schemaProps: SchemaResponseProps): SchemaResponseProps; + onSchemaSetFormErrors(errors: string[]): string[]; + updateNewFieldType(newFieldType: SchemaTypes): SchemaTypes; + onFieldUpdate({ + schema, + formUnchanged, + }: { + schema: Schema; + formUnchanged: boolean; + }): { schema: Schema; formUnchanged: boolean }; + onIndexingComplete(numDocumentsWithErrors: number): number; + resetMostRecentIndexJob(emptyReindexJob: IndexJob): IndexJob; + showFieldSuccess(successMessage: string): string; + setFieldName(rawFieldName: string): string; + setFilterValue(filterValue: string): string; + addNewField( + fieldName: string, + newFieldType: SchemaTypes + ): { fieldName: string; newFieldType: SchemaTypes }; + updateFields(): void; + openAddFieldModal(): void; + closeAddFieldModal(): void; + resetSchemaState(): void; + initializeSchema(): void; + initializeSchemaFieldErrors( + activeReindexJobId: string, + sourceId: string + ): { activeReindexJobId: string; sourceId: string }; + updateExistingFieldType( + fieldName: string, + newFieldType: SchemaTypes + ): { fieldName: string; newFieldType: SchemaTypes }; + setServerField( + updatedSchema: Schema, + operation: TOperation + ): { updatedSchema: Schema; operation: TOperation }; +} + +interface SchemaValues { + sourceId: string; + activeSchema: Schema; + serverSchema: Schema; + filterValue: string; + filteredSchemaFields: Schema; + dataTypeOptions: OptionValue[]; + showAddFieldModal: boolean; + addFieldFormErrors: string[] | null; + mostRecentIndexJob: IndexJob; + fieldCoercionErrors: FieldCoercionErrors; + newFieldType: string; + rawFieldName: string; + formUnchanged: boolean; + dataLoading: boolean; +} + +interface SchemaResponseProps { + schema: Schema; + mostRecentIndexJob: IndexJob; +} + +export interface SchemaInitialData extends SchemaResponseProps { + sourceId: string; +} + +interface FieldCoercionError { + external_id: string; + error: string; +} + +export interface FieldCoercionErrors { + [key: string]: FieldCoercionError[]; +} + +interface SchemaChangeErrorsProps { + fieldCoercionErrors: FieldCoercionErrors; +} + +const dataTypeOptions = [ + { value: 'text', text: 'Text' }, + { value: 'date', text: 'Date' }, + { value: 'number', text: 'Number' }, + { value: 'geolocation', text: 'Geo Location' }, +]; + +export const SchemaLogic = kea<MakeLogicType<SchemaValues, SchemaActions>>({ + actions: { + onInitializeSchema: (schemaProps: SchemaInitialData) => schemaProps, + onInitializeSchemaFieldErrors: (fieldCoercionErrorsProps: SchemaChangeErrorsProps) => + fieldCoercionErrorsProps, + onSchemaSetSuccess: (schemaProps: SchemaResponseProps) => schemaProps, + onSchemaSetFormErrors: (errors: string[]) => errors, + updateNewFieldType: (newFieldType: string) => newFieldType, + onFieldUpdate: ({ schema, formUnchanged }: { schema: Schema; formUnchanged: boolean }) => ({ + schema, + formUnchanged, + }), + onIndexingComplete: (numDocumentsWithErrors: number) => numDocumentsWithErrors, + resetMostRecentIndexJob: (emptyReindexJob: IndexJob) => emptyReindexJob, + showFieldSuccess: (successMessage: string) => successMessage, + setFieldName: (rawFieldName: string) => rawFieldName, + setFilterValue: (filterValue: string) => filterValue, + openAddFieldModal: () => true, + closeAddFieldModal: () => true, + resetSchemaState: () => true, + initializeSchema: () => true, + initializeSchemaFieldErrors: (activeReindexJobId: string, sourceId: string) => ({ + activeReindexJobId, + sourceId, + }), + addNewField: (fieldName: string, newFieldType: SchemaTypes) => ({ fieldName, newFieldType }), + updateExistingFieldType: (fieldName: string, newFieldType: string) => ({ + fieldName, + newFieldType, + }), + updateFields: () => true, + setServerField: (updatedSchema: Schema, operation: TOperation) => ({ + updatedSchema, + operation, + }), + }, + reducers: { + dataTypeOptions: [dataTypeOptions], + sourceId: [ + '', + { + onInitializeSchema: (_, { sourceId }) => sourceId, + }, + ], + activeSchema: [ + {}, + { + onInitializeSchema: (_, { schema }) => schema, + onSchemaSetSuccess: (_, { schema }) => schema, + onFieldUpdate: (_, { schema }) => schema, + }, + ], + serverSchema: [ + {}, + { + onInitializeSchema: (_, { schema }) => schema, + onSchemaSetSuccess: (_, { schema }) => schema, + }, + ], + mostRecentIndexJob: [ + {} as IndexJob, + { + onInitializeSchema: (_, { mostRecentIndexJob }) => mostRecentIndexJob, + resetMostRecentIndexJob: (_, emptyReindexJob) => emptyReindexJob, + onSchemaSetSuccess: (_, { mostRecentIndexJob }) => mostRecentIndexJob, + onIndexingComplete: (state, numDocumentsWithErrors) => ({ + ...state, + numDocumentsWithErrors, + percentageComplete: 100, + hasErrors: numDocumentsWithErrors > 0, + isActive: false, + }), + updateFields: (state) => ({ + ...state, + percentageComplete: 0, + }), + }, + ], + newFieldType: [ + TEXT, + { + updateNewFieldType: (_, newFieldType) => newFieldType, + onSchemaSetSuccess: () => TEXT, + }, + ], + addFieldFormErrors: [ + null, + { + onSchemaSetSuccess: () => null, + closeAddFieldModal: () => null, + onSchemaSetFormErrors: (_, addFieldFormErrors) => addFieldFormErrors, + }, + ], + filterValue: [ + '', + { + setFilterValue: (_, filterValue) => filterValue, + }, + ], + formUnchanged: [ + true, + { + onSchemaSetSuccess: () => true, + onFieldUpdate: (_, { formUnchanged }) => formUnchanged, + }, + ], + showAddFieldModal: [ + false, + { + onSchemaSetSuccess: () => false, + openAddFieldModal: () => true, + closeAddFieldModal: () => false, + }, + ], + dataLoading: [ + true, + { + onSchemaSetSuccess: () => false, + onInitializeSchema: () => false, + resetSchemaState: () => true, + }, + ], + rawFieldName: [ + '', + { + setFieldName: (_, rawFieldName) => rawFieldName, + onSchemaSetSuccess: () => '', + }, + ], + fieldCoercionErrors: [ + {}, + { + onInitializeSchemaFieldErrors: (_, { fieldCoercionErrors }) => fieldCoercionErrors, + }, + ], + }, + selectors: ({ selectors }) => ({ + filteredSchemaFields: [ + () => [selectors.activeSchema, selectors.filterValue], + (activeSchema, filterValue) => { + const filteredSchema = {} as Schema; + Object.keys(activeSchema) + .filter((x) => x.includes(filterValue)) + .forEach((k) => (filteredSchema[k] = activeSchema[k])); + return filteredSchema; + }, + ], + }), + listeners: ({ actions, values }) => ({ + initializeSchema: async () => { + const { isOrganization } = AppLogic.values; + const { http } = HttpLogic.values; + const { + contentSource: { id: sourceId }, + } = SourceLogic.values; + + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/schemas` + : `/api/workplace_search/account/sources/${sourceId}/schemas`; + + try { + const response = await http.get(route); + actions.onInitializeSchema({ sourceId, ...response }); + } catch (e) { + flashAPIErrors(e); + } + }, + initializeSchemaFieldErrors: async ({ activeReindexJobId, sourceId }) => { + const { isOrganization } = AppLogic.values; + const { http } = HttpLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/reindex_job/${activeReindexJobId}` + : `/api/workplace_search/account/sources/${sourceId}/reindex_job/${activeReindexJobId}`; + + try { + await actions.initializeSchema(); + const response = await http.get(route); + actions.onInitializeSchemaFieldErrors({ + fieldCoercionErrors: response.fieldCoercionErrors, + }); + } catch (e) { + flashAPIErrors({ ...e, message: SCHEMA_FIELD_ERRORS_ERROR_MESSAGE }); + } + }, + addNewField: ({ fieldName, newFieldType }) => { + const schema = cloneDeep(values.activeSchema); + schema[fieldName] = newFieldType; + actions.setServerField(schema, ADD); + }, + updateExistingFieldType: ({ fieldName, newFieldType }) => { + const schema = cloneDeep(values.activeSchema); + schema[fieldName] = newFieldType; + actions.onFieldUpdate({ schema, formUnchanged: isEqual(values.serverSchema, schema) }); + }, + updateFields: () => actions.setServerField(values.activeSchema, UPDATE), + setServerField: async ({ updatedSchema, operation }) => { + const { isOrganization } = AppLogic.values; + const { http } = HttpLogic.values; + const isAdding = operation === ADD; + const { sourceId } = values; + const successMessage = isAdding ? SCHEMA_FIELD_ADDED_MESSAGE : SCHEMA_UPDATED_MESSAGE; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/schemas` + : `/api/workplace_search/account/sources/${sourceId}/schemas`; + + const emptyReindexJob = { + percentageComplete: 100, + numDocumentsWithErrors: 0, + activeReindexJobId: 0, + isActive: false, + }; + + actions.resetMostRecentIndexJob(emptyReindexJob); + + try { + const response = await http.post(route, { + body: JSON.stringify({ ...updatedSchema }), + }); + actions.onSchemaSetSuccess(response); + setSuccessMessage(successMessage); + } catch (e) { + window.scrollTo(0, 0); + if (isAdding) { + actions.onSchemaSetFormErrors(e?.message); + } else { + flashAPIErrors(e); + } + } + }, + resetMostRecentIndexJob: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + resetSchemaState: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 9beac109be510..04db6bbc2912e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -8,6 +8,16 @@ import { schema } from '@kbn/config-schema'; import { RouteDependencies } from '../../plugin'; +const schemaValuesSchema = schema.recordOf( + schema.string(), + schema.oneOf([ + schema.literal('text'), + schema.literal('number'), + schema.literal('geolocation'), + schema.literal('date'), + ]) +); + const pageSchema = schema.object({ current: schema.number(), size: schema.number(), @@ -363,7 +373,7 @@ export function registerAccountSourceSchemasRoute({ { path: '/api/workplace_search/account/sources/{id}/schemas', validate: { - body: schema.object({}), + body: schemaValuesSchema, params: schema.object({ id: schema.string(), }), @@ -745,7 +755,7 @@ export function registerOrgSourceSchemasRoute({ { path: '/api/workplace_search/org/sources/{id}/schemas', validate: { - body: schema.object({}), + body: schemaValuesSchema, params: schema.object({ id: schema.string(), }),