diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 1c59d6d9aaaf815..3e6dfaefed92f1d 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=14.16.1 +ARG NODE_VERSION=14.17.0 FROM node:${NODE_VERSION} AS base diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 7eafc66465bc726..8121405e5ae249d 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -73,11 +73,7 @@ def agentProcess(Map params = [:]) { ]) { task { if (config.needBuild) { - if (!config.isXpack) { - kibanaPipeline.buildOss() - } else { - kibanaPipeline.buildXpack() - } + kibanaPipeline.buildKibana() } for(def i = 0; i < config.agentExecutions; i++) { diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress index 811af44d1ca56f8..d48b9965919dc2f 100644 --- a/.ci/Jenkinsfile_security_cypress +++ b/.ci/Jenkinsfile_security_cypress @@ -16,7 +16,7 @@ kibanaPipeline(timeoutMinutes: 180) { def job = 'xpack-securityCypress' workers.ci(name: job, size: 'l', ramDisk: true) { - kibanaPipeline.bash('test/scripts/jenkins_xpack_build_kibana.sh', 'Build Default Distributable') + kibanaPipeline.bash('test/scripts/jenkins_build_kibana.sh', 'Build Distributable') kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress_chrome.sh')() // Temporarily disabled to figure out test flake // kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress_firefox.sh')() diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index d56ec61314ac760..dc3a3cde7d658a1 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -37,12 +37,8 @@ kibanaPipeline(timeoutMinutes: 210) { ]) task { - kibanaPipeline.buildOss(6) + kibanaPipeline.buildKibana(16) tasks.ossCiGroups() - } - - task { - kibanaPipeline.buildXpack(10, true) tasks.xpackCiGroups() tasks.xpackCiGroupDocker() } diff --git a/.eslintrc.js b/.eslintrc.js index 20875a2c2913df1..a4ce657d523d955 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -711,6 +711,33 @@ module.exports = { name: 'lodash/fp/assocPath', message: 'Please use @elastic/safer-lodash-set instead', }, + { + name: 'lodash', + importNames: ['template'], + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash.template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash/template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash/fp', + importNames: ['template'], + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + name: 'lodash/fp/template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, { name: 'react-use', message: 'Please use react-use/lib/{method} instead.', @@ -730,6 +757,11 @@ module.exports = { name: 'lodash.setwith', message: 'Please use @elastic/safer-lodash-set instead', }, + { + name: 'lodash.template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, { name: 'lodash/set', message: 'Please use @elastic/safer-lodash-set instead', @@ -738,6 +770,11 @@ module.exports = { name: 'lodash/setWith', message: 'Please use @elastic/safer-lodash-set instead', }, + { + name: 'lodash/template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, ], }, ], @@ -753,6 +790,18 @@ module.exports = { property: 'set', message: 'Please use @elastic/safer-lodash-set instead', }, + { + object: 'lodash', + property: 'template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, + { + object: '_', + property: 'template', + message: + 'lodash.template is unsafe, and not compatible with our content security policy.', + }, { object: 'lodash', property: 'setWith', @@ -1576,20 +1625,5 @@ module.exports = { '@typescript-eslint/prefer-ts-expect-error': 'error', }, }, - { - files: [ - '**/public/**/*.{js,mjs,ts,tsx}', - '**/common/**/*.{js,mjs,ts,tsx}', - 'packages/**/*.{js,mjs,ts,tsx}', - ], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: ['lodash/*', '!lodash/fp', 'rxjs/internal-compatibility'], - }, - ], - }, - }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bafa023cf3f35fe..39daa5780436f3b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -188,6 +188,7 @@ /src/core/ @elastic/kibana-core /src/plugins/saved_objects_tagging_oss @elastic/kibana-core /config/kibana.yml @elastic/kibana-core +/x-pack/plugins/banners/ @elastic/kibana-core /x-pack/plugins/features/ @elastic/kibana-core /x-pack/plugins/licensing/ @elastic/kibana-core /x-pack/plugins/global_search/ @elastic/kibana-core @@ -202,7 +203,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core /src/plugins/home/public @elastic/kibana-core @@ -214,7 +214,6 @@ #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core -#CC# /src/plugins/status_page/ @elastic/kibana-core #CC# /x-pack/plugins/cloud/ @elastic/kibana-core #CC# /x-pack/plugins/features/ @elastic/kibana-core #CC# /x-pack/plugins/global_search/ @elastic/kibana-core diff --git a/.node-version b/.node-version index 6b17d228d335175..62df50f1eefe197 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.16.1 +14.17.0 diff --git a/.nvmrc b/.nvmrc index 6b17d228d335175..62df50f1eefe197 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.16.1 +14.17.0 diff --git a/Jenkinsfile b/Jenkinsfile index 4c8f126b4883b38..db5ae306e6e2efb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,7 @@ kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 210, checkPrChanges: true, setCommitStatus: true) { slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { githubPr.withDefaultPrComments { - ciStats.trackBuild(requireSuccess: githubPr.isPr()) { + ciStats.trackBuild(requireSuccess: githubPr.isTrackedBranchPr()) { catchError { retryable.enable() kibanaPipeline.allCiTasks() diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md deleted file mode 100644 index cb75452a28cd296..000000000000000 --- a/STYLEGUIDE.md +++ /dev/null @@ -1,685 +0,0 @@ -# Kibana Style Guide - -This guide applies to all development within the Kibana project and is -recommended for the development of all Kibana plugins. - -- [General](#general) -- [HTML](#html) -- [API endpoints](#api-endpoints) -- [TypeScript/JavaScript](#typeScript/javaScript) -- [SASS files](#sass-files) -- [React](#react) - -Besides the content in this style guide, the following style guides may also apply -to all development within the Kibana project. Please make sure to also read them: - -- [Accessibility style guide (EUI Docs)](https://elastic.github.io/eui/#/guidelines/accessibility) -- [SASS style guide (EUI Docs)](https://elastic.github.io/eui/#/guidelines/sass) - -## General - -### Filenames - -All filenames should use `snake_case`. - -**Right:** `src/kibana/index_patterns/index_pattern.js` - -**Wrong:** `src/kibana/IndexPatterns/IndexPattern.js` - -### Do not comment out code - -We use a version management system. If a line of code is no longer needed, -remove it, don't simply comment it out. - -### Prettier and Linting - -We are gradually moving the Kibana code base over to Prettier. All TypeScript code -and some JavaScript code (check `.eslintrc.js`) is using Prettier to format code. You -can run `node script/eslint --fix` to fix linting issues and apply Prettier formatting. -We recommend you to enable running ESLint via your IDE. - -Whenever possible we are trying to use Prettier and linting over written style guide rules. -Consider every linting rule and every Prettier rule to be also part of our style guide -and disable them only in exceptional cases and ideally leave a comment why they are -disabled at that specific place. - -## HTML - -This part contains style guide rules around general (framework agnostic) HTML usage. - -### Camel case `id` and `data-test-subj` - -Use camel case for the values of attributes such as `id` and `data-test-subj` selectors. - -```html - -``` - -The only exception is in cases where you're dynamically creating the value, and you need to use -hyphens as delimiters: - -```jsx -buttons.map(btn => ( - -) -``` - -### Capitalization in HTML and CSS should always match - -It's important that when you write CSS/SASS selectors using classes, IDs, and attributes -(keeping in mind that we should _never_ use IDs and attributes in our selectors), that the -capitalization in the CSS matches that used in the HTML. HTML and CSS follow different case sensitivity rules, and we can avoid subtle gotchas by ensuring we use the -same capitalization in both of them. - -### How to generate ids? - -When labeling elements (and for some other accessibility tasks) you will often need -ids. Ids must be unique within the page i.e. no duplicate ids in the rendered DOM -at any time. - -Since we have some components that are used multiple times on the page, you must -make sure every instance of that component has a unique `id`. To make the generation -of those `id`s easier, you can use the `htmlIdGenerator` service in the `@elastic/eui`. - -A React component could use it as follows: - -```jsx -import { htmlIdGenerator } from '@elastic/eui'; - -render() { - // Create a new generator that will create ids deterministic - const htmlId = htmlIdGenerator(); - return (
- - -
); -} -``` - -Each id generator you create by calling `htmlIdGenerator()` will generate unique but -deterministic ids. As you can see in the above example, that single generator -created the same id in the label's `htmlFor` as well as the input's `id`. - -A single generator instance will create the same id when passed the same argument -to the function multiple times. But two different generators will produce two different -ids for the same argument to the function, as you can see in the following example: - -```js -const generatorOne = htmlIdGenerator(); -const generatorTwo = htmlIdGenerator(); - -// Those statements are always true: -// Same generator -generatorOne('foo') === generatorOne('foo'); -generatorOne('foo') !== generatorOne('bar'); - -// Different generator -generatorOne('foo') !== generatorTwo('foo'); -``` - -This allows multiple instances of a single React component to now have different ids. -If you include the above React component multiple times in the same page, -each component instance will have a unique id, because each render method will use a different -id generator. - -You can also use this service outside of React. - -## API endpoints - -The following style guide rules are targeting development of server side API endpoints. - -### Paths - -API routes must start with the `/api/` path segment, and should be followed by the plugin id if applicable: - -**Right:** `/api/marvel/nodes` - -**Wrong:** `/marvel/api/nodes` - -### snake_case - -Kibana uses `snake_case` for the entire API, just like Elasticsearch. All urls, paths, query string parameters, values, and bodies should be `snake_case` formatted. - -_Right:_ - -``` -POST /api/kibana/index_patterns -{ - "id": "...", - "time_field_name": "...", - "fields": [ - ... - ] -} -``` - -## TypeScript/JavaScript - -The following style guide rules apply for working with TypeScript/JavaScript files. - -### TypeScript vs. JavaScript - -Whenever possible, write code in TypeScript instead of JavaScript, especially if it's new code. -Check out [TYPESCRIPT.md](TYPESCRIPT.md) for help with this process. - -### Prefer modern JavaScript/TypeScript syntax - -You should prefer modern language features in a lot of cases, e.g.: - -- Prefer `class` over `prototype` inheritance -- Prefer arrow function over function expressions -- Prefer arrow function over storing `this` (no `const self = this;`) -- Prefer template strings over string concatenation -- Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` -- Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) - -### Avoid mutability and state - -Wherever possible, do not rely on mutable state. This means you should not -reassign variables, modify object properties, or push values to arrays. -Instead, create new variables, and shallow copies of objects and arrays: - -```js -// good -function addBar(foos, foo) { - const newFoo = { ...foo, name: 'bar' }; - return [...foos, newFoo]; -} - -// bad -function addBar(foos, foo) { - foo.name = 'bar'; - foos.push(foo); -} -``` - -### Avoid `any` whenever possible - -Since TypeScript 3.0 and the introduction of the -[`unknown` type](https://mariusschulz.com/blog/the-unknown-type-in-typescript) there are rarely any -reasons to use `any` as a type. Nearly all places of former `any` usage can be replace by either a -generic or `unknown` (in cases the type is really not known). - -You should always prefer using those mechanisms over using `any`, since they are stricter typed and -less likely to introduce bugs in the future due to insufficient types. - -If you’re not having `any` in your plugin or are starting a new plugin, you should enable the -[`@typescript-eslint/no-explicit-any`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md) -linting rule for your plugin via the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. - -### Avoid non-null assertions - -You should try avoiding non-null assertions (`!.`) wherever possible. By using them you tell -TypeScript, that something is not null even though by it’s type it could be. Usage of non-null -assertions is most often a side-effect of you actually checked that the variable is not `null` -but TypeScript doesn’t correctly carry on that information till the usage of the variable. - -In most cases it’s possible to replace the non-null assertion by structuring your code/checks slightly different -or using [user defined type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) -to properly tell TypeScript what type a variable has. - -Using non-null assertion increases the risk for future bugs. In case the condition under which we assumed that the -variable can’t be null has changed (potentially even due to changes in compeltely different files), the non-null -assertion would now wrongly disable proper type checking for us. - -If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the -[`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) -linting rule for you plugin in the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. - -### Return/throw early from functions - -To avoid deep nesting of if-statements, always return a function's value as early -as possible. And where possible, do any assertions first: - -```js -// good -function doStuff(val) { - if (val > 100) { - throw new Error('Too big'); - } - - if (val < 0) { - return false; - } - - // ... stuff -} - -// bad -function doStuff(val) { - if (val >= 0) { - if (val < 100) { - // ... stuff - } else { - throw new Error('Too big'); - } - } else { - return false; - } -} -``` - -### Use object destructuring - -This helps avoid temporary references and helps prevent typo-related bugs. - -```js -// best -function fullName({ first, last }) { - return `${first} ${last}`; -} - -// good -function fullName(user) { - const { first, last } = user; - return `${first} ${last}`; -} - -// bad -function fullName(user) { - const first = user.first; - const last = user.last; - return `${first} ${last}`; -} -``` - -### Use array destructuring - -Directly accessing array values via index should be avoided, but if it is -necessary, use array destructuring: - -```js -const arr = [1, 2, 3]; - -// good -const [first, second] = arr; - -// bad -const first = arr[0]; -const second = arr[1]; -``` - -### Magic numbers/strings - -These are numbers (or other values) simply used in line in your code. _Do not -use these_, give them a variable name so they can be understood and changed -easily. - -```js -// good -const minWidth = 300; - -if (width < minWidth) { - ... -} - -// bad -if (width < 300) { - ... -} -``` - -### Modules - -Module dependencies should be written using native ES2015 syntax wherever -possible (which is almost everywhere): - -```js -// good -import { mapValues } from 'lodash'; -export mapValues; - -// bad -const _ = require('lodash'); -module.exports = _.mapValues; - -// worse -define(['lodash'], function (_) { - ... -}); -``` - -In those extremely rare cases where you're writing server-side JavaScript in a -file that does not pass run through webpack, then use CommonJS modules. - -In those even rarer cases where you're writing client-side code that does not -run through webpack, then do not use a module loader at all. - -#### Import only top-level modules - -The files inside a module are implementation details of that module. They -should never be imported directly. Instead, you must only import the top-level -API that's exported by the module itself. - -Without a clear mechanism in place in JS to encapsulate protected code, we make -a broad assumption that anything beyond the root of a module is an -implementation detail of that module. - -On the other hand, a module should be able to import parent and sibling -modules. - -```js -// good -import foo from 'foo'; -import child from './child'; -import parent from '../'; -import ancestor from '../../../'; -import sibling from '../foo'; - -// bad -import inFoo from 'foo/child'; -import inSibling from '../foo/child'; -``` - -### Global definitions - -Don't do this. Everything should be wrapped in a module that can be depended on -by other modules. Even things as simple as a single value should be a module. - -### Only use ternary operators for small, simple code - -And _never_ use multiple ternaries together, because they make it more -difficult to reason about how different values flow through the conditions -involved. Instead, structure the logic for maximum readability. - -```js -// good, a situation where only 1 ternary is needed -const foo = a === b ? 1 : 2; - -// bad -const foo = a === b ? 1 : a === c ? 2 : 3; -``` - -### Use descriptive conditions - -Any non-trivial conditions should be converted to functions or assigned to -descriptively named variables. By breaking up logic into smaller, -self-contained blocks, it becomes easier to reason about the higher-level -logic. Additionally, these blocks become good candidates for extraction into -their own modules, with unit-tests. - -```js -// best -function isShape(thing) { - return thing instanceof Shape; -} -function notSquare(thing) { - return !(thing instanceof Square); -} -if (isShape(thing) && notSquare(thing)) { - ... -} - -// good -const isShape = thing instanceof Shape; -const notSquare = !(thing instanceof Square); -if (isShape && notSquare) { - ... -} - -// bad -if (thing instanceof Shape && !(thing instanceof Square)) { - ... -} -``` - -### Name regular expressions - -```js -// good -const validPassword = /^(?=.*\d).{4,}$/; - -if (password.length >= 4 && validPassword.test(password)) { - console.log('password is valid'); -} - -// bad -if (password.length >= 4 && /^(?=.*\d).{4,}$/.test(password)) { - console.log('losing'); -} -``` - -### Write small functions - -Keep your functions short. A good function fits on a slide that the people in -the last row of a big room can comfortably read. So don't count on them having -perfect vision and limit yourself to ~15 lines of code per function. - -### Use "rest" syntax rather than built-in `arguments` - -For expressiveness sake, and so you can be mix dynamic and explicit arguments. - -```js -// good -function something(foo, ...args) { - ... -} - -// bad -function something(foo) { - const args = Array.from(arguments).slice(1); - ... -} -``` - -### Default argument syntax - -Always use the default argument syntax for optional arguments. - -```js -// good -function foo(options = {}) { - ... -} - -// bad -function foo(options) { - if (typeof options === 'undefined') { - options = {}; - } - ... -} -``` - -And put your optional arguments at the end. - -```js -// good -function foo(bar, options = {}) { - ... -} - -// bad -function foo(options = {}, bar) { - ... -} -``` - -### Use thunks to create closures, where possible - -For trivial examples (like the one that follows), thunks will seem like -overkill, but they encourage isolating the implementation details of a closure -from the business logic of the calling code. - -```js -// good -function connectHandler(client, callback) { - return () => client.connect(callback); -} -setTimeout(connectHandler(client, afterConnect), 1000); - -// not as good -setTimeout(() => { - client.connect(afterConnect); -}, 1000); - -// bad -setTimeout(() => { - client.connect(() => { - ... - }); -}, 1000); -``` - -### Use slashes for comments - -Use slashes for both single line and multi line comments. Try to write -comments that explain higher level mechanisms or clarify difficult -segments of your code. _Don't use comments to restate trivial things_. - -_Exception:_ Comment blocks describing a function and its arguments -(docblock) should start with `/**`, contain a single `*` at the beginning of -each line, and end with `*/`. - -```js -// good - -// 'ID_SOMETHING=VALUE' -> ['ID_SOMETHING=VALUE', 'SOMETHING', 'VALUE'] -const matches = item.match(/ID_([^\n]+)=([^\n]+)/)); - -/** - * Fetches a user from... - * @param {string} id - id of the user - * @return {Promise} - */ -function loadUser(id) { - // This function has a nasty side effect where a failure to increment a - // redis counter used for statistics will cause an exception. This needs - // to be fixed in a later iteration. - - ... -} - -const isSessionValid = (session.expires < Date.now()); -if (isSessionValid) { - ... -} - -// bad - -// Execute a regex -const matches = item.match(/ID_([^\n]+)=([^\n]+)/)); - -// Usage: loadUser(5, function() { ... }) -function loadUser(id, cb) { - // ... -} - -// Check if the session is valid -const isSessionValid = (session.expires < Date.now()); -// If the session is valid -if (isSessionValid) { - ... -} -``` - -### Getters and Setters - -Feel free to use getters that are free from [side effects][sideeffect], like -providing a length property for a collection class. - -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). - -All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/core/public/core_app/styles/_globals_v7light.scss). - -While the styles for this component will only be loaded if the component exists on the page, -the styles **will** be global and so it is recommended to use a three letter prefix on your -classes to ensure proper scope. - -**Example:** - -```tsx -// component.tsx - -import './component.scss'; -// All other imports below the SASS import - -export const Component = () => { - return ( -
- ); -} -``` - -```scss -// component.scss - -.plgComponent { ... } -``` - -Do not use the underscore `_` SASS file naming pattern when importing directly into a javascript file. - -## React - -The following style guide rules are specific for working with the React framework. - -### Prefer reactDirective over react-component - -When using `ngReact` to embed your react components inside Angular HTML, prefer the -`reactDirective` service over the `react-component` directive. -You can read more about these two ngReact methods [here](https://github.com/ngReact/ngReact#features). - -Using `react-component` means adding a bunch of components into angular, while `reactDirective` keeps them isolated, and is also a more succinct syntax. - -**Good:** - -```html - -``` - -**Bad:** - -```html - -``` - -### Action function names and prop function names - -Name action functions in the form of a strong verb and passed properties in the form of on. E.g: - -```jsx - - -``` - -## Attribution - -Parts of the JavaScript style guide were initially forked from the -[node style guide](https://github.com/felixge/node-style-guide) created by [Felix Geisendörfer](http://felixge.de/) which is -licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) -license. diff --git a/STYLEGUIDE.mdx b/STYLEGUIDE.mdx new file mode 100644 index 000000000000000..afe00476640b3c6 --- /dev/null +++ b/STYLEGUIDE.mdx @@ -0,0 +1,695 @@ +--- +id: kibStyleGuide +slug: /kibana-dev-docs/styleguide +title: Style Guide +summary: JavaScript/TypeScript styleguide. +date: 2021-05-06 +tags: ['kibana', 'onboarding', 'dev', 'styleguide', 'typescript', 'javascript'] +--- + +This guide applies to all development within the Kibana project and is +recommended for the development of all Kibana plugins. + +Besides the content in this style guide, the following style guides may also apply +to all development within the Kibana project. Please make sure to also read them: + +- [Accessibility style guide (EUI Docs)](https://elastic.github.io/eui/#/guidelines/accessibility) +- [SASS style guide (EUI Docs)](https://elastic.github.io/eui/#/guidelines/sass) + +## General + +### Filenames + +All filenames should use `snake_case`. + +**Right:** `src/kibana/index_patterns/index_pattern.js` + +**Wrong:** `src/kibana/IndexPatterns/IndexPattern.js` + +### Do not comment out code + +We use a version management system. If a line of code is no longer needed, +remove it, don't simply comment it out. + +### Prettier and Linting + +We are gradually moving the Kibana code base over to Prettier. All TypeScript code +and some JavaScript code (check `.eslintrc.js`) is using Prettier to format code. You +can run `node script/eslint --fix` to fix linting issues and apply Prettier formatting. +We recommend you to enable running ESLint via your IDE. + +Whenever possible we are trying to use Prettier and linting over written style guide rules. +Consider every linting rule and every Prettier rule to be also part of our style guide +and disable them only in exceptional cases and ideally leave a comment why they are +disabled at that specific place. + +## HTML + +This part contains style guide rules around general (framework agnostic) HTML usage. + +### Camel case `id` and `data-test-subj` + +Use camel case for the values of attributes such as `id` and `data-test-subj` selectors. + +```html + +``` + +The only exception is in cases where you're dynamically creating the value, and you need to use +hyphens as delimiters: + +```jsx +buttons.map(btn => ( + +) +``` + +### Capitalization in HTML and CSS should always match + +It's important that when you write CSS/SASS selectors using classes, IDs, and attributes +(keeping in mind that we should _never_ use IDs and attributes in our selectors), that the +capitalization in the CSS matches that used in the HTML. HTML and CSS follow different case sensitivity rules, and we can avoid subtle gotchas by ensuring we use the +same capitalization in both of them. + +### How to generate ids? + +When labeling elements (and for some other accessibility tasks) you will often need +ids. Ids must be unique within the page i.e. no duplicate ids in the rendered DOM +at any time. + +Since we have some components that are used multiple times on the page, you must +make sure every instance of that component has a unique `id`. To make the generation +of those `id`s easier, you can use the `htmlIdGenerator` service in the `@elastic/eui`. + +A React component could use it as follows: + +```jsx +import { htmlIdGenerator } from '@elastic/eui'; + +render() { + // Create a new generator that will create ids deterministic + const htmlId = htmlIdGenerator(); + return (
+ + +
); +} +``` + +Each id generator you create by calling `htmlIdGenerator()` will generate unique but +deterministic ids. As you can see in the above example, that single generator +created the same id in the label's `htmlFor` as well as the input's `id`. + +A single generator instance will create the same id when passed the same argument +to the function multiple times. But two different generators will produce two different +ids for the same argument to the function, as you can see in the following example: + +```js +const generatorOne = htmlIdGenerator(); +const generatorTwo = htmlIdGenerator(); + +// Those statements are always true: +// Same generator +generatorOne('foo') === generatorOne('foo'); +generatorOne('foo') !== generatorOne('bar'); + +// Different generator +generatorOne('foo') !== generatorTwo('foo'); +``` + +This allows multiple instances of a single React component to now have different ids. +If you include the above React component multiple times in the same page, +each component instance will have a unique id, because each render method will use a different +id generator. + +You can also use this service outside of React. + +## API endpoints + +The following style guide rules are targeting development of server side API endpoints. + +### Paths + +API routes must start with the `/api/` path segment, and should be followed by the plugin id if applicable: + +**Right:** `/api/marvel/nodes` + +**Wrong:** `/marvel/api/nodes` + +### snake_case + +Kibana uses `snake_case` for the entire API, just like Elasticsearch. All urls, paths, query string parameters, values, and bodies should be `snake_case` formatted. + +_Right:_ + +``` +POST /api/kibana/index_patterns +{ + "id": "...", + "time_field_name": "...", + "fields": [ + ... + ] +} +``` + +## TypeScript/JavaScript + +The following style guide rules apply for working with TypeScript/JavaScript files. + +### TypeScript vs. JavaScript + +Whenever possible, write code in TypeScript instead of JavaScript, especially if it's new code. +Check out [TYPESCRIPT.md](TYPESCRIPT.md) for help with this process. + +### Prefer modern JavaScript/TypeScript syntax + +You should prefer modern language features in a lot of cases, e.g.: + +- Prefer `class` over `prototype` inheritance +- Prefer arrow function over function expressions +- Prefer arrow function over storing `this` (no `const self = this;`) +- Prefer template strings over string concatenation +- Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` +- Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) + +### Avoid mutability and state + +Wherever possible, do not rely on mutable state. This means you should not +reassign variables, modify object properties, or push values to arrays. +Instead, create new variables, and shallow copies of objects and arrays: + +```js +// good +function addBar(foos, foo) { + const newFoo = { ...foo, name: 'bar' }; + return [...foos, newFoo]; +} + +// bad +function addBar(foos, foo) { + foo.name = 'bar'; + foos.push(foo); +} +``` + +### Avoid `any` whenever possible + +Since TypeScript 3.0 and the introduction of the +[`unknown` type](https://mariusschulz.com/blog/the-unknown-type-in-typescript) there are rarely any +reasons to use `any` as a type. Nearly all places of former `any` usage can be replace by either a +generic or `unknown` (in cases the type is really not known). + +You should always prefer using those mechanisms over using `any`, since they are stricter typed and +less likely to introduce bugs in the future due to insufficient types. + +If you’re not having `any` in your plugin or are starting a new plugin, you should enable the +[`@typescript-eslint/no-explicit-any`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md) +linting rule for your plugin via the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. + +### Avoid non-null assertions + +You should try avoiding non-null assertions (`!.`) wherever possible. By using them you tell +TypeScript, that something is not null even though by it’s type it could be. Usage of non-null +assertions is most often a side-effect of you actually checked that the variable is not `null` +but TypeScript doesn’t correctly carry on that information till the usage of the variable. + +In most cases it’s possible to replace the non-null assertion by structuring your code/checks slightly different +or using [user defined type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) +to properly tell TypeScript what type a variable has. + +Using non-null assertion increases the risk for future bugs. In case the condition under which we assumed that the +variable can’t be null has changed (potentially even due to changes in compeltely different files), the non-null +assertion would now wrongly disable proper type checking for us. + +If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the +[`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) +linting rule for you plugin in the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. + +### Return/throw early from functions + +To avoid deep nesting of if-statements, always return a function's value as early +as possible. And where possible, do any assertions first: + +```js +// good +function doStuff(val) { + if (val > 100) { + throw new Error('Too big'); + } + + if (val < 0) { + return false; + } + + // ... stuff +} + +// bad +function doStuff(val) { + if (val >= 0) { + if (val < 100) { + // ... stuff + } else { + throw new Error('Too big'); + } + } else { + return false; + } +} +``` + +### Use object destructuring + +This helps avoid temporary references and helps prevent typo-related bugs. + +```js +// best +function fullName({ first, last }) { + return `${first} ${last}`; +} + +// good +function fullName(user) { + const { first, last } = user; + return `${first} ${last}`; +} + +// bad +function fullName(user) { + const first = user.first; + const last = user.last; + return `${first} ${last}`; +} +``` + +### Use array destructuring + +Directly accessing array values via index should be avoided, but if it is +necessary, use array destructuring: + +```js +const arr = [1, 2, 3]; + +// good +const [first, second] = arr; + +// bad +const first = arr[0]; +const second = arr[1]; +``` + +### Magic numbers/strings + +These are numbers (or other values) simply used in line in your code. _Do not +use these_, give them a variable name so they can be understood and changed +easily. + +```js +// good +const minWidth = 300; + +if (width < minWidth) { + ... +} + +// bad +if (width < 300) { + ... +} +``` + +### Modules + +Module dependencies should be written using native ES2015 syntax wherever +possible (which is almost everywhere): + +```js +// good +import { mapValues } from 'lodash'; +export mapValues; + +// bad +const _ = require('lodash'); +module.exports = _.mapValues; + +// worse +define(['lodash'], function (_) { + ... +}); +``` + +In those extremely rare cases where you're writing server-side JavaScript in a +file that does not pass run through webpack, then use CommonJS modules. + +In those even rarer cases where you're writing client-side code that does not +run through webpack, then do not use a module loader at all. + +#### Import only top-level modules + +The files inside a module are implementation details of that module. They +should never be imported directly. Instead, you must only import the top-level +API that's exported by the module itself. + +Without a clear mechanism in place in JS to encapsulate protected code, we make +a broad assumption that anything beyond the root of a module is an +implementation detail of that module. + +On the other hand, a module should be able to import parent and sibling +modules. + +```js +// good +import foo from 'foo'; +import child from './child'; +import parent from '../'; +import ancestor from '../../../'; +import sibling from '../foo'; + +// bad +import inFoo from 'foo/child'; +import inSibling from '../foo/child'; +``` + +#### Avoid export \* in top level index.ts files + +The exports in `common/index.ts`, `public/index.ts` and `server/index.ts` dictate a plugin's public API. The public API should be carefully controlled, and using `export *` makes it very easy for a developer working on internal changes to export a new public API unintentionally. + +```js +// good +export { foo } from 'foo'; +export { child } from './child'; + +// bad +export * from 'foo/child'; +export * from '../foo/child'; +``` + +### Global definitions + +Don't do this. Everything should be wrapped in a module that can be depended on +by other modules. Even things as simple as a single value should be a module. + +### Only use ternary operators for small, simple code + +And _never_ use multiple ternaries together, because they make it more +difficult to reason about how different values flow through the conditions +involved. Instead, structure the logic for maximum readability. + +```js +// good, a situation where only 1 ternary is needed +const foo = a === b ? 1 : 2; + +// bad +const foo = a === b ? 1 : a === c ? 2 : 3; +``` + +### Use descriptive conditions + +Any non-trivial conditions should be converted to functions or assigned to +descriptively named variables. By breaking up logic into smaller, +self-contained blocks, it becomes easier to reason about the higher-level +logic. Additionally, these blocks become good candidates for extraction into +their own modules, with unit-tests. + +```js +// best +function isShape(thing) { + return thing instanceof Shape; +} +function notSquare(thing) { + return !(thing instanceof Square); +} +if (isShape(thing) && notSquare(thing)) { + ... +} + +// good +const isShape = thing instanceof Shape; +const notSquare = !(thing instanceof Square); +if (isShape && notSquare) { + ... +} + +// bad +if (thing instanceof Shape && !(thing instanceof Square)) { + ... +} +``` + +### Name regular expressions + +```js +// good +const validPassword = /^(?=.*\d).{4,}$/; + +if (password.length >= 4 && validPassword.test(password)) { + console.log('password is valid'); +} + +// bad +if (password.length >= 4 && /^(?=.*\d).{4,}$/.test(password)) { + console.log('losing'); +} +``` + +### Write small functions + +Keep your functions short. A good function fits on a slide that the people in +the last row of a big room can comfortably read. So don't count on them having +perfect vision and limit yourself to ~15 lines of code per function. + +### Use "rest" syntax rather than built-in `arguments` + +For expressiveness sake, and so you can be mix dynamic and explicit arguments. + +```js +// good +function something(foo, ...args) { + ... +} + +// bad +function something(foo) { + const args = Array.from(arguments).slice(1); + ... +} +``` + +### Default argument syntax + +Always use the default argument syntax for optional arguments. + +```js +// good +function foo(options = {}) { + ... +} + +// bad +function foo(options) { + if (typeof options === 'undefined') { + options = {}; + } + ... +} +``` + +And put your optional arguments at the end. + +```js +// good +function foo(bar, options = {}) { + ... +} + +// bad +function foo(options = {}, bar) { + ... +} +``` + +### Use thunks to create closures, where possible + +For trivial examples (like the one that follows), thunks will seem like +overkill, but they encourage isolating the implementation details of a closure +from the business logic of the calling code. + +```js +// good +function connectHandler(client, callback) { + return () => client.connect(callback); +} +setTimeout(connectHandler(client, afterConnect), 1000); + +// not as good +setTimeout(() => { + client.connect(afterConnect); +}, 1000); + +// bad +setTimeout(() => { + client.connect(() => { + ... + }); +}, 1000); +``` + +### Use slashes for comments + +Use slashes for both single line and multi line comments. Try to write +comments that explain higher level mechanisms or clarify difficult +segments of your code. _Don't use comments to restate trivial things_. + +_Exception:_ Comment blocks describing a function and its arguments +(docblock) should start with `/**`, contain a single `*` at the beginning of +each line, and end with `*/`. + +```js +// good + +// 'ID_SOMETHING=VALUE' -> ['ID_SOMETHING=VALUE', 'SOMETHING', 'VALUE'] +const matches = item.match(/ID_([^\n]+)=([^\n]+)/)); + +/** + * Fetches a user from... + * @param {string} id - id of the user + * @return {Promise} + */ +function loadUser(id) { + // This function has a nasty side effect where a failure to increment a + // redis counter used for statistics will cause an exception. This needs + // to be fixed in a later iteration. + + ... +} + +const isSessionValid = (session.expires < Date.now()); +if (isSessionValid) { + ... +} + +// bad + +// Execute a regex +const matches = item.match(/ID_([^\n]+)=([^\n]+)/)); + +// Usage: loadUser(5, function() { ... }) +function loadUser(id, cb) { + // ... +} + +// Check if the session is valid +const isSessionValid = (session.expires < Date.now()); +// If the session is valid +if (isSessionValid) { + ... +} +``` + +### Getters and Setters + +Feel free to use getters that are free from [side effects][sideeffect], like +providing a length property for a collection class. + +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). + +All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/core/public/core_app/styles/_globals_v7light.scss). + +While the styles for this component will only be loaded if the component exists on the page, +the styles **will** be global and so it is recommended to use a three letter prefix on your +classes to ensure proper scope. + +**Example:** + +```tsx +// component.tsx + +import './component.scss'; +// All other imports below the SASS import + +export const Component = () => { + return
; +}; +``` + +```scss +// component.scss + +.plgComponent { ... } +``` + +Do not use the underscore `_` SASS file naming pattern when importing directly into a javascript file. + +## React + +The following style guide rules are specific for working with the React framework. + +### Prefer reactDirective over react-component + +When using `ngReact` to embed your react components inside Angular HTML, prefer the +`reactDirective` service over the `react-component` directive. +You can read more about these two ngReact methods [here](https://github.com/ngReact/ngReact#features). + +Using `react-component` means adding a bunch of components into angular, while `reactDirective` keeps them isolated, and is also a more succinct syntax. + +**Good:** + +```html + +``` + +**Bad:** + +```html + +``` + +### Action function names and prop function names + +Name action functions in the form of a strong verb and passed properties in the form of `on`. E.g: + +```jsx + + +``` + +## Attribution + +Parts of the JavaScript style guide were initially forked from the +[node style guide](https://github.com/felixge/node-style-guide) created by [Felix Geisendörfer](http://felixge.de/) which is +licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) +license. diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index d334d7979ed591b..d80ad948cbb553a 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "65067dcad93a61deb593be7d3d9a32a4577d09665536d8da536d731da5cd15e2", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.4.2/rules_nodejs-3.4.2.tar.gz"], + sha256 = "10f534e1c80f795cffe1f2822becd4897754d18564612510c59b3c73544ae7c6", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.0/rules_nodejs-3.5.0.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.4.2") +check_rules_nodejs_version(minimum_version_string = "3.5.0") # Setup the Node.js toolchain for the architectures we want to support # @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.4.2") # we can update that rule. node_repositories( node_repositories = { - "14.16.1-darwin_amd64": ("node-v14.16.1-darwin-x64.tar.gz", "node-v14.16.1-darwin-x64", "b762b72fc149629b7e394ea9b75a093cad709a9f2f71480942945d8da0fc1218"), - "14.16.1-linux_arm64": ("node-v14.16.1-linux-arm64.tar.xz", "node-v14.16.1-linux-arm64", "b4d474e79f7d33b3b4430fad25c3f836b82ce2d5bb30d4a2c9fa20df027e40da"), - "14.16.1-linux_s390x": ("node-v14.16.1-linux-s390x.tar.xz", "node-v14.16.1-linux-s390x", "af9982fef32e4a3e4a5d66741dcf30ac9c27613bd73582fa1dae1fb25003047a"), - "14.16.1-linux_amd64": ("node-v14.16.1-linux-x64.tar.xz", "node-v14.16.1-linux-x64", "85a89d2f68855282c87851c882d4c4bbea4cd7f888f603722f0240a6e53d89df"), - "14.16.1-windows_amd64": ("node-v14.16.1-win-x64.zip", "node-v14.16.1-win-x64", "e469db37b4df74627842d809566c651042d86f0e6006688f0f5fe3532c6dfa41"), + "14.17.0-darwin_amd64": ("node-v14.17.0-darwin-x64.tar.gz", "node-v14.17.0-darwin-x64", "7b210652e11d1ee25650c164cf32381895e1dcb3e0ff1d0841d8abc1f47ac73e"), + "14.17.0-linux_arm64": ("node-v14.17.0-linux-arm64.tar.xz", "node-v14.17.0-linux-arm64", "712e5575cee20570a0a56f4d4b4572cb0f2ee2f4bce49433de18be0393e7df22"), + "14.17.0-linux_s390x": ("node-v14.17.0-linux-s390x.tar.xz", "node-v14.17.0-linux-s390x", "6419372b9e9ad37e0bce188dc5740f2f060aaa44454418e462b4088a310a1c0b"), + "14.17.0-linux_amd64": ("node-v14.17.0-linux-x64.tar.xz", "node-v14.17.0-linux-x64", "494b161759a3d19c70e3172d33ce1918dd8df9ad20d29d1652a8387a84e2d308"), + "14.17.0-windows_amd64": ("node-v14.17.0-win-x64.zip", "node-v14.17.0-win-x64", "6582a7259c433e9f667dcc4ed3e5d68bc514caba2eed40e4626c8b4c7e5ecd5c"), }, - node_version = "14.16.1", + node_version = "14.17.0", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/api_docs/features.json b/api_docs/features.json index 7ee722488221319..427525db6c4168c 100644 --- a/api_docs/features.json +++ b/api_docs/features.json @@ -2101,7 +2101,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 33 + "lineNumber": 41 }, "deprecated": false, "children": [ @@ -2125,7 +2125,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 34 + "lineNumber": 42 }, "deprecated": false, "children": [ @@ -2147,7 +2147,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 34 + "lineNumber": 42 }, "deprecated": false, "isRequired": true @@ -2175,7 +2175,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 35 + "lineNumber": 43 }, "deprecated": false, "children": [ @@ -2197,7 +2197,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 35 + "lineNumber": 43 }, "deprecated": false, "isRequired": true @@ -2225,7 +2225,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 41 + "lineNumber": 49 }, "deprecated": false, "children": [], @@ -2251,7 +2251,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 47 + "lineNumber": 55 }, "deprecated": false, "children": [], @@ -2270,7 +2270,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 48 + "lineNumber": 56 }, "deprecated": false, "children": [], @@ -2288,11 +2288,47 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 56 + "lineNumber": 64 }, "deprecated": false, "children": [], "returnComment": [] + }, + { + "parentPluginId": "features", + "id": "def-server.PluginSetupContract.featurePrivilegeIterator", + "type": "Function", + "tags": [], + "label": "featurePrivilegeIterator", + "description": [ + "\nUtility for iterating through all privileges belonging to a specific feature.\n{@see FeaturePrivilegeIterator }" + ], + "signature": [ + "FeaturePrivilegeIterator" + ], + "source": { + "path": "x-pack/plugins/features/server/plugin.ts", + "lineNumber": 70 + }, + "deprecated": false + }, + { + "parentPluginId": "features", + "id": "def-server.PluginSetupContract.subFeaturePrivilegeIterator", + "type": "Function", + "tags": [], + "label": "subFeaturePrivilegeIterator", + "description": [ + "\nUtility for iterating through all sub-feature privileges belonging to a specific feature.\n{@see SubFeaturePrivilegeIterator }" + ], + "signature": [ + "SubFeaturePrivilegeIterator" + ], + "source": { + "path": "x-pack/plugins/features/server/plugin.ts", + "lineNumber": 76 + }, + "deprecated": false } ], "initialIsOpen": false @@ -2306,7 +2342,7 @@ "description": [], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 59 + "lineNumber": 79 }, "deprecated": false, "children": [ @@ -2330,7 +2366,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 60 + "lineNumber": 80 }, "deprecated": false, "children": [], @@ -2356,7 +2392,7 @@ ], "source": { "path": "x-pack/plugins/features/server/plugin.ts", - "lineNumber": 61 + "lineNumber": 81 }, "deprecated": false, "children": [], diff --git a/api_docs/spaces.json b/api_docs/spaces.json index d53b69d5bd6b55a..940bbcf88a484f8 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1867,7 +1867,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" + ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"collectMultiNamespaceReferences\" | \"updateObjectsSpaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "source": { "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts", diff --git a/api_docs/spaces_oss.json b/api_docs/spaces_oss.json index bce557db8e51673..a0ed4297ddc39bc 100644 --- a/api_docs/spaces_oss.json +++ b/api_docs/spaces_oss.json @@ -577,15 +577,11 @@ "deprecated": false, "children": [ { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApi.activeSpace$", - "type": "Object", - "tags": [], - "label": "activeSpace$", - "description": [ - "\nObservable representing the currently active space.\nThe details of the space can change without a full page reload (such as display name, color, etc.)" - ], + "id": "def-public.SpacesApi.getActiveSpace$", + "type": "Function", + "label": "getActiveSpace$", "signature": [ + "() => ", "Observable", "<", { @@ -597,11 +593,16 @@ }, ">" ], + "description": [ + "\nObservable representing the currently active space.\nThe details of the space can change without a full page reload (such as display name, color, etc.)" + ], + "children": [], + "tags": [], + "returnComment": [], "source": { "path": "src/plugins/spaces_oss/public/api.ts", "lineNumber": 22 - }, - "deprecated": false + } }, { "parentPluginId": "spacesOss", diff --git a/dev_docs/assets/ml_csv_upload.png b/dev_docs/assets/ml_csv_upload.png new file mode 100644 index 000000000000000..dd82e8d40fcc832 Binary files /dev/null and b/dev_docs/assets/ml_csv_upload.png differ diff --git a/dev_docs/assets/sample_data.png b/dev_docs/assets/sample_data.png new file mode 100644 index 000000000000000..69bf138f9a1d793 Binary files /dev/null and b/dev_docs/assets/sample_data.png differ diff --git a/dev_docs/building_blocks.mdx b/dev_docs/building_blocks.mdx index 95851ea66b8cb58..327492a20d5b887 100644 --- a/dev_docs/building_blocks.mdx +++ b/dev_docs/building_blocks.mdx @@ -74,7 +74,7 @@ Check out the Map Embeddable if you wish to embed a map in your application. All Kibana pages should use KibanaPageTemplate to setup their pages. It's a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements. -Check out for more implementation guidance. +Check out for more implementation guidance. **Github labels**: `EUI` diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index ec78fa49aa231e8..aa38890a8ac9ecc 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -1,13 +1,13 @@ --- -id: kibDevDocsKBLTutorial -slug: /kibana-dev-docs/tutorials/kibana-page-layout -title: KibanaPageLayout component +id: kibDevDocsKPTTutorial +slug: /kibana-dev-docs/tutorials/kibana-page-template +title: KibanaPageTemplate component summary: Learn how to create pages in Kibana date: 2021-03-20 tags: ['kibana', 'dev', 'ui', 'tutorials'] --- -`KibanaPageLayout` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. +`KibanaPageTemplate` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. Refer to EUI's documentation on [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. @@ -18,7 +18,7 @@ Use the `isEmptyState` prop for when there is no page content to show. For examp The default empty state uses any `pageHeader` info provided to populate an [`EuiEmptyPrompt`](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. ```tsx - + No data} body="You have no data. Would you like some of ours?" @@ -55,7 +55,7 @@ You can also provide a custom empty prompt to replace the pre-built one. You'll , ]} /> - + ``` ![Screenshot of demo custom empty state code. Shows the Kibana navigation bars and a centered empty state with the a level 1 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](../assets/kibana_custom_empty_state.png) @@ -65,7 +65,7 @@ You can also provide a custom empty prompt to replace the pre-built one. You'll When passing both a `pageHeader` configuration and `isEmptyState`, the component will render the proper template (`centeredContent`). Be sure to reduce the heading level within your child empty prompt to `

`. ```tsx -, ]} /> - + ``` ![Screenshot of demo custom empty state code with a page header. Shows the Kibana navigation bars, a level 1 heading "Dashboards", and a centered empty state with the a level 2 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](../assets/kibana_header_and_empty_state.png) diff --git a/dev_docs/tutorials/sample_data.mdx b/dev_docs/tutorials/sample_data.mdx new file mode 100644 index 000000000000000..75afaaaea6f327b --- /dev/null +++ b/dev_docs/tutorials/sample_data.mdx @@ -0,0 +1,33 @@ +--- +id: kibDevTutorialSampleData +slug: /kibana-dev-docs/tutorial/sample-data +title: Add sample data +summary: Learn how to add sample data to Kibana +date: 2021-04-26 +tags: ['kibana', 'onboarding', 'dev', 'architecture', 'tutorials'] +--- + +## Installation from the UI + +1. Navigate to the home page. +2. Click **Add data**. +3. Click on the **Sample data** tab. +4. Select a dataset by clicking on the **Add data** button. + +![Sample Data](../assets/sample_data.png) + +## CSV Upload + +1. Navigate to the **Machine Learning** application. +2. Click on the **Data Visualizer** tab. +3. Click on **Select file** in the **Import data** container. + +![CSV Upload](../assets/ml_csv_upload.png) + +## makelogs + +The makelogs script generates sample web server logs. Make sure Elasticsearch is running before running the script. + +```sh +node scripts/makelogs --auth : +``` \ No newline at end of file diff --git a/docs/concepts/index-patterns.asciidoc b/docs/concepts/index-patterns.asciidoc index 158fa6282e6fa64..03bad72a317c6b5 100644 --- a/docs/concepts/index-patterns.asciidoc +++ b/docs/concepts/index-patterns.asciidoc @@ -10,10 +10,9 @@ or all indices that contain your data. It can also point to a You’ll learn how to: -* Create an index pattern -* Explore and configure the data fields +* Create index patterns * Set the default index pattern -* Delete an index pattern +* Delete index patterns [float] [[index-patterns-read-only-access]] @@ -133,77 +132,23 @@ To exclude a cluster, use `cluster_*:logstash-*,cluster_one:-*`. Once an index pattern is configured using the {ccs} syntax, all searches and aggregations using that index pattern in {kib} take advantage of {ccs}. - -[float] -[[reload-fields]] -=== Explore and configure the data fields - -To explore and configure the data fields in your index pattern, open the main menu, then click -*Stack Management > Index Patterns*. Each field has a {ref}/mapping.html[mapping], -which indicates the type of data the field contains in {es}, -such as strings or boolean values. The field mapping also determines -how you can use the field, such as whether it can be searched or aggregated. - -When a new field is added to the index, the index pattern field list is updated -the next time the index pattern is loaded, for example, when you load the page or -move between {kib} apps. - -[role="screenshot"] -image:management/index-patterns/images/new-index-pattern.png["Create index pattern"] - -[float] -=== Format the display of common field types - -Whenever possible, {kib} uses the same field type for display as -{es}. However, some field types that {es} supports are not available -in {kib}. Using field formatters, you can manually change the field type in {kib} to display your data the way you prefer -to see it, regardless of how it is stored in {es}. - -For example, if you store -date values in {es}, you can use a {kib} field formatter to change the display to mm/dd/yyyy format. -{kib} has field formatters for -<>, -<>, -<>, -and <>. - -To customize the displayed field name provided by {es}, you can -use *Custom Label* . - -A popularity counter keeps track of the fields you use most often. -The top five most popular fields and their values are displayed in <>. - -To edit the field display, click the edit icon -(image:management/index-patterns/images/edit_icon.png[]) in the index pattern detail view. - -[role="screenshot"] -image:management/index-patterns/images/edit-field-format.png["Edit field format"] - [float] -[[default-index-pattern]] -=== Set the default index pattern +[[delete-index-pattern]] +=== Delete index patterns -The first index pattern you create is automatically designated as the default pattern, -but you can set any index pattern as the default. The default index pattern is automatically selected when you first open <> or create a visualization from scratch. +When you delete an index pattern, you are unable to recover the associated field formatters, scripted fields, source filters, +and field popularity data. Deleting an index pattern does not remove any indices or data documents from {es}. -. In *Index patterns*, click the index pattern name. -. Click the star icon (image:management/index-patterns/images/star.png[Star icon]). +WARNING: Deleting an index pattern breaks all visualizations, saved searches, and other saved objects that reference the index pattern. -[float] -[[delete-index-pattern]] -=== Delete an index pattern +. Open the main menu, then click *Stack Management > Index Patterns*. -This action removes the pattern from the list of saved objects in {kib}. -You will not be able to recover field formatters, scripted fields, source filters, -and field popularity data associated with the index pattern. Deleting an -index pattern does not remove any indices or data documents from {es}. +. Click the index pattern you want to delete. -WARNING: Deleting an index pattern breaks all visualizations, saved searches, and other saved objects that reference the pattern. - -. In *Index patterns*, click the index pattern name. -. Click the delete icon (image:management/index-patterns/images/delete.png[Delete icon]). +. Delete (image:management/index-patterns/images/delete.png[Delete icon]) the index pattern. [float] +[[reload-fields]] === What’s next -* Learn about <> and how to create data on the fly. +Learn how to <> in your index patterns. diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc index 74e5bd4d4fb2f8a..cb37dceb535649b 100644 --- a/docs/concepts/index.asciidoc +++ b/docs/concepts/index.asciidoc @@ -49,10 +49,9 @@ that accesses the {kib} API. {kib} uses the index pattern to show you a list of fields, such as `event.duration`. You can customize the display name and format for each field. -For example, you can tell Kibana to display `event.duration` in seconds. +For example, you can tell {kib} to display `event.duration` in seconds. {kib} has <> for strings, -dates, geopoints, -and numbers. +dates, geopoints, and numbers. [float] [[kibana-concepts-searching-your-data]] diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index cba5b9bfadf98d8..e81875d7893dd8b 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -74,17 +74,24 @@ yarn kbn watch-bazel - @kbn/config-schema - @kbn/crypto - @kbn/dev-utils +- @kbn/docs-utils - @kbn/es - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint - @kbn/expect +- @kbn/i18n - @kbn/legacy-logging - @kbn/logging - @kbn/securitysolution-constants -- @kbn/securitysolution-utils +- @kbn/securitysolution-es-utils +- kbn/securitysolution-io-ts-alerting-types +- kbn/securitysolution-io-ts-list-types +- kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils +- @kbn/securitysolution-utils +- @kbn/server-http-tools - @kbn/std +- @kbn/telemetry-utils - @kbn/tinymath - @kbn/utility-types - @kbn/utils - diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 067b6b206fa1447..4ba5e32eec8b536 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -181,6 +181,10 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s oss plugins. +|{kib-repo}blob/{branch}/src/plugins/screenshot_mode/README.md[screenshotMode] +|The service exposed by this plugin informs consumers whether they should optimize for non-interactivity. In this way plugins can avoid loading unnecessary code, data or other services. + + |{kib-repo}blob/{branch}/src/plugins/security_oss/README.md[securityOss] |securityOss is responsible for educating users about Elastic's free security features, so they can properly protect the data within their clusters. diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 180d376ceaf51c3..0448ad42c94fa92 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -144,6 +144,7 @@ readonly links: { createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; + createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 91ef8358b5fd227..78d2d8daa3d4575 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b868a7f8216df69..5280d85f3d3b38a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -103,12 +103,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttributes](./kibana-plugin-core-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | | [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) | | | [SavedObjectReference](./kibana-plugin-core-public.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. | | [SavedObjectsBaseOptions](./kibana-plugin-core-public.savedobjectsbaseoptions.md) | | | [SavedObjectsBatchResponse](./kibana-plugin-core-public.savedobjectsbatchresponse.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-public.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkCreateOptions](./kibana-plugin-core-public.savedobjectsbulkcreateoptions.md) | | | [SavedObjectsBulkUpdateObject](./kibana-plugin-core-public.savedobjectsbulkupdateobject.md) | | | [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.md) | | +| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. | | [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) | | | [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | | | [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md new file mode 100644 index 000000000000000..10e01d7e7a9316b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) + +## SavedObjectReferenceWithContext.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md new file mode 100644 index 000000000000000..722b11f0c7ba9fb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) + +## SavedObjectReferenceWithContext.inboundReferences property + +References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation + +Signature: + +```typescript +inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md new file mode 100644 index 000000000000000..8a4b378850764ad --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) + +## SavedObjectReferenceWithContext.isMissing property + +Whether or not this object or reference is missing + +Signature: + +```typescript +isMissing?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md new file mode 100644 index 000000000000000..a79fa96695e36b0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) + +## SavedObjectReferenceWithContext interface + +A returned input object or one of its references, with additional context. + +Signature: + +```typescript +export interface SavedObjectReferenceWithContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md new file mode 100644 index 000000000000000..9140e94721f1e3b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) + +## SavedObjectReferenceWithContext.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md new file mode 100644 index 000000000000000..02b0c9c0949df44 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) + +## SavedObjectReferenceWithContext.spacesWithMatchingAliases property + +The space(s) that legacy URL aliases matching this type/id exist in + +Signature: + +```typescript +spacesWithMatchingAliases?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md new file mode 100644 index 000000000000000..d2e341627153caf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) + +## SavedObjectReferenceWithContext.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md new file mode 100644 index 000000000000000..a6e0a274008a655 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse interface + +The response when object references are collected. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md new file mode 100644 index 000000000000000..66a7a19d18288a9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) > [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectReferenceWithContext[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md index 4778c98493b5bd4..1daaf95a73d5dd3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md index 6d3f8df2fa51828..1031d733fed4ab7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md index ed64d61e75fabc9..fc1748d4db90799 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md index 1d837d9b4705d69..ce40bd7c750f019 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md index 8eeb5ef638a8294..d9d1c6c3edb41ef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md index 2398410fa4b8405..90aa2f0100d8832 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -16,5 +16,6 @@ export interface ElasticsearchStatusMeta | Property | Type | Description | | --- | --- | --- | | [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | +| [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) | NodesVersionCompatibility['nodesInfoRequestError'] | | | [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md new file mode 100644 index 000000000000000..1b46078a1a45316 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) + +## ElasticsearchStatusMeta.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md index b5fbb3d54b972a0..d1e87feba0f0334 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). 7.16 > Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md index 4da121984d08470..c004ad2548802ff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md). +> Use [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md). 7.16 > Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md index 51d0b2e4882cb6b..8e7ecdb9f7ec2e5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md). +> Use [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md). 7.16 > Serves the same purpose as "normal" `ClusterClient` but exposes additional `callAsCurrentUser` method that doesn't use credentials of the Kibana internal user (as `callAsInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API. diff --git a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md index 706898c4ad9aa28..9103f9cfc674052 100644 --- a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md index 1755ff40c2bc06e..29d4668becffce5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md @@ -9,5 +9,5 @@ An async generator which wraps calls to `savedObjectsClient.find` and iterates o Signature: ```typescript -find: () => AsyncGenerator; +find: () => AsyncGenerator>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md index 4686df18e01343a..950d6c078654cad 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface ISavedObjectsPointInTimeFinder +export interface ISavedObjectsPointInTimeFinder ``` ## Properties @@ -16,5 +16,5 @@ export interface ISavedObjectsPointInTimeFinder | Property | Type | Description | | --- | --- | --- | | [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | () => Promise<void> | Closes the Point-In-Time associated with this finder instance.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | -| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | +| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse<T, A>> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md index 168209659046e7d..2378e61484da5ab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md index 40def157114ef22..219180af26fd83f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > The set of options that defines how API call should be made and result be processed. diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index 0872e5ba7c2197e..05855c31477c3e2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). 7.16 > Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md index 7c53356615ee9fa..7cf696ad8d73f2e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md @@ -4,7 +4,7 @@ ## LegacyElasticsearchError interface -@deprecated. The new elasticsearch client doesn't wrap errors anymore. +@deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md index 7517acc59ac8011..0f2d653e41a5545 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [IScopedClusterClient.asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md). +> Use [IScopedClusterClient.asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md). 7.16 > Calls specified `endpoint` with provided `clientParams` on behalf of the user initiated request to the Kibana server (via HTTP request headers). See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md index b683d3945f9ff02..2c184b0fde5b329 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [IScopedClusterClient.asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md). +> Use [IScopedClusterClient.asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md). 7.16 > Calls specified `endpoint` with provided `clientParams` on behalf of the Kibana internal user. See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index 6b6649e833a92d4..6678c3bc16d531d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Use [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md). +> Use [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md). 7.16 > Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 4df8d074ba9c87d..d638b84224e2345 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -108,7 +108,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | +| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | | [LoggerContextConfigInput](./kibana-plugin-core-server.loggercontextconfiginput.md) | | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | @@ -144,8 +144,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | | [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions, and they cannot exceed the current Kibana version.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | -| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | | -| [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) | | +| [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. | | [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | | @@ -158,13 +157,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) | | +| [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) | An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the namespaceType: 'multi' or namespaceType: 'multi-isolated' option).Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the namespaceType: 'multi'). | +| [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) | Options for collecting references. | +| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. | | [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | -| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | -| [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | @@ -208,6 +208,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) | | | [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-core-server.savedobjectstype.md)'s management section. | | [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | +| [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) | An object that should have its spaces updated. | +| [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) | Options for the update operation. | +| [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) | The response when objects' spaces are updated. | +| [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) | Details about a specific object's update result. | | [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | | | [SearchResponse](./kibana-plugin-core-server.searchresponse.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md index a924f0cea6b6b0c..ea0a277931eaf18 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md index 0fcae8c847cb40c..f71e6e78a4c34ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md @@ -6,6 +6,7 @@ > Warning: This API is now obsolete. > +> 7.16 > Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md index 6fcfacc3bc9085e..cbdac9d5455b01c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -18,5 +18,6 @@ export interface NodesVersionCompatibility | [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | | [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | | [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | +| [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) | Error | | | [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md new file mode 100644 index 000000000000000..aa9421afed6e84a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) + +## NodesVersionCompatibility.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: Error; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md new file mode 100644 index 000000000000000..8ac532c601efcb6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) + +## SavedObjectExportBaseOptions.includeNamespaces property + +Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. + +Signature: + +```typescript +includeNamespaces?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md index 0e8fa73039d4008..cd0c352086425c2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md @@ -16,6 +16,7 @@ export interface SavedObjectExportBaseOptions | Property | Type | Description | | --- | --- | --- | | [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | +| [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) | boolean | Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. | | [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | | [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | | [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | KibanaRequest | The http request initiating the export. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md index 2a30693f4da84a2..9fe43a2f3f477d8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md @@ -9,5 +9,5 @@ The version in which this object type is being converted to a multi-namespace ty Signature: ```typescript -convertToMultiNamespaceTypeVersion?: string; +readonly convertToMultiNamespaceTypeVersion?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md index a1b3378afc53b2f..20a0e99275a39e5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md @@ -9,5 +9,5 @@ logger instance to be used by the migration handler Signature: ```typescript -log: SavedObjectsMigrationLogger; +readonly log: SavedObjectsMigrationLogger; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md index 7b20ae41048f666..a1c2717e6e4a02f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md @@ -9,5 +9,5 @@ The migration version that this migration function is defined for Signature: ```typescript -migrationVersion: string; +readonly migrationVersion: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md new file mode 100644 index 000000000000000..7ef1a2fb1bd411d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) + +## SavedObjectReferenceWithContext.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md new file mode 100644 index 000000000000000..058c27032d06534 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) + +## SavedObjectReferenceWithContext.inboundReferences property + +References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation + +Signature: + +```typescript +inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md new file mode 100644 index 000000000000000..d46d5a6bf2a0a8d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) + +## SavedObjectReferenceWithContext.isMissing property + +Whether or not this object or reference is missing + +Signature: + +```typescript +isMissing?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md new file mode 100644 index 000000000000000..1f8b33c6e94e8d2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) + +## SavedObjectReferenceWithContext interface + +A returned input object or one of its references, with additional context. + +Signature: + +```typescript +export interface SavedObjectReferenceWithContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md new file mode 100644 index 000000000000000..2c2114103b29a7b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) + +## SavedObjectReferenceWithContext.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md new file mode 100644 index 000000000000000..07f4158a8495047 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) + +## SavedObjectReferenceWithContext.spacesWithMatchingAliases property + +The space(s) that legacy URL aliases matching this type/id exist in + +Signature: + +```typescript +spacesWithMatchingAliases?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md new file mode 100644 index 000000000000000..118d9744e4276e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) + +## SavedObjectReferenceWithContext.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md deleted file mode 100644 index 711588bdd608cd0..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) - -## SavedObjectsAddToNamespacesOptions interface - - -Signature: - -```typescript -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | -| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md deleted file mode 100644 index c0a1008ab533161..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) - -## SavedObjectsAddToNamespacesOptions.refresh property - -The Elasticsearch Refresh setting for this operation - -Signature: - -```typescript -refresh?: MutatingOperationRefreshSetting; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md deleted file mode 100644 index 9432b4bf80da6c6..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) - -## SavedObjectsAddToNamespacesOptions.version property - -An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. - -Signature: - -```typescript -version?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md deleted file mode 100644 index 306f502f0b0b3a4..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) - -## SavedObjectsAddToNamespacesResponse interface - - -Signature: - -```typescript -export interface SavedObjectsAddToNamespacesResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) | string[] | The namespaces the object exists in after this operation is complete. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md deleted file mode 100644 index 4fc2e376304d485..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) > [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) - -## SavedObjectsAddToNamespacesResponse.namespaces property - -The namespaces the object exists in after this operation is complete. - -Signature: - -```typescript -namespaces: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md deleted file mode 100644 index 567390faba9b279..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) - -## SavedObjectsClient.addToNamespaces() method - -Adds namespaces to a SavedObject - -Signature: - -```typescript -addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsAddToNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md new file mode 100644 index 000000000000000..155167d32a73834 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) + +## SavedObjectsClient.collectMultiNamespaceReferences() method + +Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. + +Signature: + +```typescript +collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md index 8afd9634645741c..39d09807e4f3b08 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md @@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call ` Signature: ```typescript -createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; ``` ## Parameters @@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, Returns: -`ISavedObjectsPointInTimeFinder` +`ISavedObjectsPointInTimeFinder` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md deleted file mode 100644 index 18ef5c3e6350c0a..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) - -## SavedObjectsClient.deleteFromNamespaces() method - -Removes namespaces from a SavedObject - -Signature: - -```typescript -deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsDeleteFromNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 95c2251f72c9004..2e293889b179445 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -25,20 +25,20 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | -| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md).Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | +| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) | | Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.The generator wraps calls to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | -| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | | [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | +| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md new file mode 100644 index 000000000000000..7ababbbe1f535e0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) + +## SavedObjectsClient.updateObjectsSpaces() method + +Updates one or more objects to add and/or remove them from specified spaces. + +Signature: + +```typescript +updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsUpdateObjectsSpacesObject[] | | +| spacesToAdd | string[] | | +| spacesToRemove | string[] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md new file mode 100644 index 000000000000000..21522a0f32d6d00 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) > [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md new file mode 100644 index 000000000000000..e675658f2bf7666 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject interface + +An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the `namespaceType: 'multi'` or `namespaceType: 'multi-isolated'` option). + +Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the `namespaceType: 'multi'`). + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md new file mode 100644 index 000000000000000..c376a9e4258c803 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) > [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md new file mode 100644 index 000000000000000..9311a6626975339 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) + +## SavedObjectsCollectMultiNamespaceReferencesOptions interface + +Options for collecting references. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) | 'collectMultiNamespaceReferences' | 'updateObjectsSpaces' | Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md new file mode 100644 index 000000000000000..a36301a6451bc5e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) > [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) + +## SavedObjectsCollectMultiNamespaceReferencesOptions.purpose property + +Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' + +Signature: + +```typescript +purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md new file mode 100644 index 000000000000000..bc72e7399446819 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse interface + +The response when object references are collected. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md new file mode 100644 index 000000000000000..4b5707d7228a55c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) > [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectReferenceWithContext[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md deleted file mode 100644 index 8a2afe6656fa4c4..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) - -## SavedObjectsDeleteFromNamespacesOptions interface - - -Signature: - -```typescript -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md deleted file mode 100644 index 1175b79bc1abdf5..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) - -## SavedObjectsDeleteFromNamespacesOptions.refresh property - -The Elasticsearch Refresh setting for this operation - -Signature: - -```typescript -refresh?: MutatingOperationRefreshSetting; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md deleted file mode 100644 index 6021c8866f01841..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) - -## SavedObjectsDeleteFromNamespacesResponse interface - - -Signature: - -```typescript -export interface SavedObjectsDeleteFromNamespacesResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) | string[] | The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md deleted file mode 100644 index 9600a9e89138011..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) > [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) - -## SavedObjectsDeleteFromNamespacesResponse.namespaces property - -The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. - -Signature: - -```typescript -namespaces: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md deleted file mode 100644 index 4b69b10318ed3b3..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) - -## SavedObjectsRepository.addToNamespaces() method - -Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. - -Signature: - -```typescript -addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsAddToNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md new file mode 100644 index 000000000000000..450cd14a2052435 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) + +## SavedObjectsRepository.collectMultiNamespaceReferences() method + +Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. + +Signature: + +```typescript +collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md index 5d9d2857f6e0b17..c92a1986966fd86 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md @@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call ` Signature: ```typescript -createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; ``` ## Parameters @@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, Returns: -`ISavedObjectsPointInTimeFinder` +`ISavedObjectsPointInTimeFinder` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md deleted file mode 100644 index d5ffb6d9ff9d888..000000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) - -## SavedObjectsRepository.deleteFromNamespaces() method - -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. - -Signature: - -```typescript -deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsDeleteFromNamespacesOptions | | - -Returns: - -`Promise` - 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 00e6ed3aeddfcb4..191b125ef3f74bf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -15,17 +15,16 @@ export declare class SavedObjectsRepository | Method | Modifiers | Description | | --- | --- | --- | -| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[deleteFromNamespaces\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using openPointInTimeForType.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. | +| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) | | Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.This generator wraps calls to [SavedObjectsRepository.find()](./kibana-plugin-core-server.savedobjectsrepository.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | -| [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, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. | @@ -33,4 +32,5 @@ export declare class SavedObjectsRepository | [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. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | +| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md new file mode 100644 index 000000000000000..6914c1b46b8296a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) + +## SavedObjectsRepository.updateObjectsSpaces() method + +Updates one or more objects to add and/or remove them from specified spaces. + +Signature: + +```typescript +updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsUpdateObjectsSpacesObject[] | | +| spacesToAdd | string[] | | +| spacesToRemove | string[] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md index 3fc386f26314173..d71db9caf6a3b82 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md @@ -9,7 +9,7 @@ Converts a document from the format that is stored in elasticsearch to the saved Signature: ```typescript -rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; +rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; ``` ## Parameters @@ -21,5 +21,5 @@ rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptio Returns: -`SavedObjectSanitizedDoc` +`SavedObjectSanitizedDoc` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md new file mode 100644 index 000000000000000..dac110ac4f47545 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) > [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) + +## SavedObjectsUpdateObjectsSpacesObject.id property + +The type of the object to update + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md new file mode 100644 index 000000000000000..847e40a8896b43b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) + +## SavedObjectsUpdateObjectsSpacesObject interface + +An object that should have its spaces updated. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) | string | The type of the object to update | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) | string | The ID of the object to update | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md new file mode 100644 index 000000000000000..2e54d1636c5e9b8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) > [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) + +## SavedObjectsUpdateObjectsSpacesObject.type property + +The ID of the object to update + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md new file mode 100644 index 000000000000000..49ee013c5d2da96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) + +## SavedObjectsUpdateObjectsSpacesOptions interface + +Options for the update operation. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md new file mode 100644 index 000000000000000..3d210f6ac51c766 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) + +## SavedObjectsUpdateObjectsSpacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md new file mode 100644 index 000000000000000..bf53277887bda32 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) + +## SavedObjectsUpdateObjectsSpacesResponse interface + +The response when objects' spaces are updated. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) | SavedObjectsUpdateObjectsSpacesResponseObject[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md new file mode 100644 index 000000000000000..13328e2aed094dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) > [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) + +## SavedObjectsUpdateObjectsSpacesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md new file mode 100644 index 000000000000000..7d7ac4ada884ddc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.error property + +Included if there was an error updating this object's spaces + +Signature: + +```typescript +error?: SavedObjectError; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md new file mode 100644 index 000000000000000..28a81ee5dfd6a6f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md new file mode 100644 index 000000000000000..03802278ee5a3f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject interface + +Details about a specific object's update result. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesResponseObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) | SavedObjectError | Included if there was an error updating this object's spaces | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) | string | The ID of the referenced object | +| [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md new file mode 100644 index 000000000000000..52b1ca187925c43 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md new file mode 100644 index 000000000000000..da0bbb10885073e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 2b3d3df1ec8d0eb..4e3dea5549b566c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -4,6 +4,11 @@ ## IFieldType interface +> Warning: This API is now obsolete. +> +> Use IndexPatternField or FieldSpec instead +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 3a78395b4275488..bf7f88ab3703951 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -4,7 +4,10 @@ ## IIndexPattern interface -IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided +> Warning: This API is now obsolete. +> +> IIndexPattern allows for an IndexPattern OR an index pattern saved object Use IndexPattern or IndexPatternSpec instead +> Signature: 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 58a225a3a4bc3f3..7f5a042e0ab8187 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 @@ -67,7 +67,7 @@ | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) | | -| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided | +| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | | | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 259009c1c5668da..3c99ae4c86c639a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -21,7 +21,7 @@ search: { })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; - Ipv4Address: typeof Ipv4Address; + IpAddress: typeof IpAddress; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; isNumberType: (agg: import("../common").AggConfig) => boolean; isStringType: (agg: import("../common").AggConfig) => boolean; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 48836a1b620b8de..5ac48d26a85d6d3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -4,6 +4,11 @@ ## IFieldType interface +> Warning: This API is now obsolete. +> +> Use IndexPatternField or FieldSpec instead +> + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 118b0104fbee640..7559695a0a33165 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index f4404521561d24b..dd1f3806c140816 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md index 930f7710f9a0092..7072f25489db298 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md @@ -21,7 +21,7 @@ search: { })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; - Ipv4Address: typeof Ipv4Address; + IpAddress: typeof IpAddress; isNumberType: (agg: import("../common").AggConfig) => boolean; isStringType: (agg: import("../common").AggConfig) => boolean; isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean; diff --git a/docs/discover/images/add-field-to-pattern.png b/docs/discover/images/add-field-to-pattern.png new file mode 100644 index 000000000000000..84dfcb0745c6915 Binary files /dev/null and b/docs/discover/images/add-field-to-pattern.png differ diff --git a/docs/discover/images/hello-field.png b/docs/discover/images/hello-field.png new file mode 100644 index 000000000000000..07d97e054d7ecbe Binary files /dev/null and b/docs/discover/images/hello-field.png differ diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 6cdb1dbfa712e51..ec5677bd04a6e25 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -111,6 +111,13 @@ image::images/connector-select-type.png[Connector select type] === Importing and exporting connectors To import and export rules, use the <>. +After a successful import, the proper banner is displayed: +[role="screenshot"] +image::images/coonectors-import-banner.png[Connectors import banner, width=50%] + +If a connector is missing user sensitive information because of the import, a **Fix** button appears in the list view. +[role="screenshot"] +image::images/connectors-with-missing-secrets.png[Connectors with missing secrets] [float] [[create-connectors]] diff --git a/docs/management/field-formatters/color-formatter.asciidoc b/docs/management/field-formatters/color-formatter.asciidoc index d9ba5e9be116573..488fb3715379970 100644 --- a/docs/management/field-formatters/color-formatter.asciidoc +++ b/docs/management/field-formatters/color-formatter.asciidoc @@ -1,10 +1,5 @@ -The `Color` field formatter enables you to specify colors with specific ranges of values for a numeric field. +The *Color* field formatter enables you to specify colors with ranges of values for a number field. -When you select the `Color` field formatter, Kibana displays the *Range*, *Font Color*, *Background Color*, and -*Example* fields. - -Click the *Add Color* button to add a range of values to associate with a particular color. You can click in the *Font -Color* and *Background Color* fields to display a color picker. You can also enter a specific hex code value in the -field. The effect of your current color choices are displayed in the *Example* field. +When you select the *Color* formatter, click *Add Color*, then specify the *Range*, *Text color*, and *Background color*. image::images/colorformatter.png[] diff --git a/docs/management/field-formatters/duration-formatter.asciidoc b/docs/management/field-formatters/duration-formatter.asciidoc index 36a73f61f622747..873a4ac94c2911a 100644 --- a/docs/management/field-formatters/duration-formatter.asciidoc +++ b/docs/management/field-formatters/duration-formatter.asciidoc @@ -1,4 +1,4 @@ -The `Duration` field formatter can display the numeric value of a field in the following increments: +The *Duration* field formatter displays the numeric value of a field in the following increments: * Picoseconds * Nanoseconds @@ -12,4 +12,4 @@ The `Duration` field formatter can display the numeric value of a field in the f * Months * Years -You can specify these increments with up to 20 decimal places for both input and output formats. +You can specify these increments with up to 20 decimal places for input and output formats. diff --git a/docs/management/field-formatters/string-formatter.asciidoc b/docs/management/field-formatters/string-formatter.asciidoc index ed3aa45873284ef..f32eee7dc8396b8 100644 --- a/docs/management/field-formatters/string-formatter.asciidoc +++ b/docs/management/field-formatters/string-formatter.asciidoc @@ -1,11 +1,20 @@ -The `String` field formatter can apply the following transformations to the field's contents: +The *String* field formatter enables you to apply transforms to the field. + +Supported transformations include: * Convert to lowercase + * Convert to uppercase + * Convert to title case -* Apply the short dots transformation, which replaces the content before a `.` character with the first character of -that content, as in the following example: + +* Apply the short dots transformation, which replaces the content before the `.` character with the first character of +the content. For example: [horizontal] *Original*:: *Becomes* `com.organizations.project.ClassName`:: `c.o.p.ClassName` + +* Base64 decode + +* URL param decode diff --git a/docs/management/field-formatters/url-formatter.asciidoc b/docs/management/field-formatters/url-formatter.asciidoc index 41d4f75603dc698..8b0e43c9f249687 100644 --- a/docs/management/field-formatters/url-formatter.asciidoc +++ b/docs/management/field-formatters/url-formatter.asciidoc @@ -1,33 +1,32 @@ -The `Url` field formatter can take on the following types: +You can specify the following types to the `Url` field formatter: -* The *Link* type turn the contents of the field into an URL. -* The *Image* type can be used to specify an image directory where a specified image is located. -* The *Audio* type can be used to specify an audio directory where a specified audio file is located. +* *Link* — Converts the contents of the field into an URL. You can specify the width and height of the image, while keeping the aspect ratio. +When the image is smaller than the specified paramters, the image is unable to upscale. +* *Image* — Specifies the image directory. +* *Audio* — Specify the audio directory. -For an *Image* type you can specify width and height attributes. These will be used to set the max width / max height of the image, while keeping the aspect ratio. Image will not be upscaled if it's smaller than the provided size parameters. - -You can customize either type of URL field formats with templates. A _URL template_ enables you to add specific values -to a partial URL. Use the string `{{value}}` to add the contents of the field to a fixed URL. +To customize URL field formats, use templates. An *URL template* enables you to add values +to a partial URL. To add the contents of the field to a fixed URL, use the `{{value}}` string. For example, when: * A field contains a user ID -* That field uses the `Url` field formatter +* A field uses the `Url` field formatter * The URI template is `http://company.net/profiles?user_id={­{value}­}` The resulting URL replaces `{{value}}` with the user ID from the field. The `{{value}}` template string URL-encodes the contents of the field. When a field encoded into a URL contains -non-ASCII characters, these characters are replaced with a `%` character and the appropriate hexadecimal code. For +non-ASCII characters, the characters are replaced with a `%` character and the appropriate hexadecimal code. For example, field contents `users/admin` result in the URL template adding `users%2Fadmin`. -When the formatter type is set to *Image*, the `{{value}}` template string specifies the name of an image at the +When the formatter type is *Image*, the `{{value}}` template string specifies the name of an image at the specified URI. -When the formatter type is set to *Audio*, the `{{value}}` template string specifies the name of an audio file at the specified URI. +When the formatter type is *Audio*, the `{{value}}` template string specifies the name of an audio file at the specified URI. -In order to pass unescaped values directly to the URL, use the `{{rawValue}}` string. +To pass unescaped values directly to the URL, use the `{{rawValue}}` string. -A _Label Template_ enables you to specify a text string that displays instead of the raw URL. You can use the +A *Label template* enables you to specify a text string that appears instead of the raw URL. You can use the `{{value}}` template string normally in label templates. You can also use the `{{url}}` template string to display the formatted URL. diff --git a/docs/management/images/colorformatter.png b/docs/management/images/colorformatter.png index df5dc34dd31e51a..3c2cfad62d76a0f 100644 Binary files a/docs/management/images/colorformatter.png and b/docs/management/images/colorformatter.png differ diff --git a/docs/management/images/connectors-with-missing-secrets.png b/docs/management/images/connectors-with-missing-secrets.png new file mode 100644 index 000000000000000..ffc902d4a476874 Binary files /dev/null and b/docs/management/images/connectors-with-missing-secrets.png differ diff --git a/docs/management/images/coonectors-import-banner.png b/docs/management/images/coonectors-import-banner.png new file mode 100644 index 000000000000000..55a6e91d28c8df1 Binary files /dev/null and b/docs/management/images/coonectors-import-banner.png differ diff --git a/docs/management/manage-index-patterns.asciidoc b/docs/management/manage-index-patterns.asciidoc new file mode 100644 index 000000000000000..94870733174ad49 --- /dev/null +++ b/docs/management/manage-index-patterns.asciidoc @@ -0,0 +1,264 @@ +[[managing-index-patterns]] +== Manage index pattern data fields + +To customize the data fields in your index pattern, you can add runtime fields to the existing documents, add scrited fields to compute data on the fly, and change how {kib} displays the data fields. + +[float] +[[runtime-fields]] +=== Explore your data with runtime fields + +Runtime fields are fields that you add to documents after you've ingested, and are evaluated at query time. With runtime fields, you allow for a smaller index and faster ingest time so that you can use less resources and reduce your operating costs. You can use runtime fields anywhere index patterns are used. + +When you use runtime fields, you can: + +* Define fields for a specific use without modifying the underlying schema. + +* Override the returned values from index fields. + +* Start working on your data without first understanding the structure. + +* Add fields to existing documents without reindexing your data. + +* Explore runtime field data in *Discover*. + +* Create visualizations with runtime field data using *Lens*, *Maps*, and *TSVB*. + +WARNING: Runtime fields can impact {kib} performance. When you run a query, {es} uses the fields you index first to shorten the response time. +Index the fields that you commonly search for and filter on, such as `timestamp`, then use runtime fields to limit the number of fields {es} uses to calculate values. + +For more information, refer to {ref}/runtime.html[Runtime fields]. + +[float] +[[create-runtime-fields]] +==== Create runtime fields + +Create runtime fields in your index patterns, or create runtime fields in *Discover* and *Lens*. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern you want to add the runtime field to, then click *Add field*. + +. Enter a *Name* for the runtime field, then select the field *Type*. + +. Select *Set value*, then define the field value by emitting a single value using the {ref}/modules-scripting-painless.html[Painless scripting language]. ++ +The script must match the field *Type*, or the script fails. + +. Click *Create field*. +//+ +//For information on how to create runtime fields in *Discover*, refer to <>. ++ +For information on how to create runtime fields in *Lens*, refer to <>. + +[float] +[[runtime-field-examples]] +==== Runtime field examples + +Try the runtime field examples on your own using the *Sample web logs* data index pattern. + +[float] +[[simple-hello-world-example]] +==== Return a keyword value + +To return `Hello World!` value: + +[source,text] +---- +emit("Hello World!"); +---- + +[float] +[[perform-a-calculation-on-a-single-field]] +===== Perform a calculation on a single field + +Calculate kilobytes from bytes: + +[source,text] +---- +emit(doc['bytes'].value / 1024) +---- + +[float] +[[return-substring]] +===== Return a substring + +Return the string that appears after the last slash in the URL: + +[source,text] +---- +def path = doc["url.keyword"].value; +if (path != null) { + int lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex > 0) { + emit(path.substring(lastSlashIndex+1)); + return; + } +} +emit(""); +---- + +[float] +[[replace-nulls-with-blanks]] +===== Replace nulls with blanks + +Replace null values with none values: + +[source,text] +---- +def source = doc['referer'].value; +if (source != null) { + emit(source); + return; +} +else { + emit("None"); +} +---- + +Specify operating system condition: + +[source,text] +---- +def source = doc['machine.os.keyword'].value; +if (source != "") { + emit(source); +} +else { + emit("None"); +} +---- + +[float] +[[manage-runtime-fields]] +==== Manage runtime fields + +Edit the settings for runtime fields, or remove runtime fields from index patterns. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. + +[float] +[[scripted-fields]] +=== Add scripted fields to index patterns + +deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] + +Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on +the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default +query language. + +WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on +{kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are +buggy, you'll get exceptions whenever you try to view the dynamically generated data. + +When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the +{ref}/modules-scripting-painless.html[Painless] scripting language. + +You can reference any single value numeric field in your expressions, for example: + +---- +doc['field_name'].value +---- + +For more information on scripted fields and additional examples, refer to +https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] + +[float] +[[create-scripted-field]] +==== Create scripted fields + +Create and add scripted fields to your index patterns. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern you want to add a scripted field to. + +. Select the *Scripted fields* tab, then click *Add scripted field*. + +. Enter a *Name* for the scripted field, then enter the *Script* you want to use to compute a value on the fly from your index data. + +. Click *Create field*. + +For more information about scripted fields in {es}, refer to {ref}/modules-scripting.html[Scripting]. + +[float] +[[update-scripted-field]] +==== Manage scripted fields + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Select the index pattern that contains the scripted field you want to manage. + +. Select the *Scripted fields* tab, then open the scripted field edit options or delete the scripted field. + +WARNING: Built-in validation is unsupported for scripted fields. When your scripts contain errors, you receive +exceptions when you view the dynamically generated data. + +[float] +[[managing-fields]] +=== Format data fields + +{kib} uses the same field types as {es}, however, some {es} field types are unsupported in {kib}. +To customize how {kib} displays data fields, use the formatting options. + +. Open the main menu, then click *Stack Management > Index Patterns*. + +. Click the index pattern that contains the field you want to change. + +. Find the field, then open the edit options (image:management/index-patterns/images/edit_icon.png[Data field edit icon]). + +. Select *Set custom label*, then enter a *Custom label* for the field. + +. Select *Set format*, then enter the *Format* for the field. + +[float] +[[string-field-formatters]] +==== String field formatters + +String fields support *String* and *Url* formatters. + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/url-formatter.asciidoc[] + +[float] +[[field-formatters-date]] +==== Date field formatters + +Date fields support *Date*, *String*, and *Url* formatters. + +The *Date* formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] +standard format definitions. + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/url-formatter.asciidoc[] + +[float] +[[field-formatters-geopoint]] +==== Geographic point field formatters + +Geographic point fields support the *String* formatter. + +include::field-formatters/string-formatter.asciidoc[] + +[float] +[[field-formatters-numeric]] +==== Number field formatters + +Numeric fields support *Bytes*, *Color*, *Duration*, *Histogram*, *Number*, *Percentage*, *String*, and *Url* formatters. + +The *Bytes*, *Number*, and *Percentage* formatters enable you to choose the display formats of numbers in the field using +the <> syntax that {kib} maintains. + +The *Histogram* formatter is used only for the {ref}/histogram.html[histogram field type]. When you use the *Histogram* formatter, +you can apply the *Bytes*, *Number*, or *Percentage* format to aggregated data. + +include::field-formatters/url-formatter.asciidoc[] + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/duration-formatter.asciidoc[] + +include::field-formatters/color-formatter.asciidoc[] \ No newline at end of file diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc deleted file mode 100644 index 505f6853c79060a..000000000000000 --- a/docs/management/managing-fields.asciidoc +++ /dev/null @@ -1,134 +0,0 @@ -[[managing-fields]] -== Field management - -Whenever possible, -{kib} uses the same field type for display as {es}. However, a few field types -{es} supports are not available in {kib}. Use field formatters to customize how your -fields are displayed in Kibana, regardless of how they are stored in {es}. - -Kibana provides these field formatters: - -* <> -* <> -* <> -* <> - -To format a field: - -. Open the main menu, and click *Stack Management > Index Patterns*. -. Click the index pattern that contains the field you want to format. -. Find the field you want to format and click the edit icon (image:management/index-patterns/images/edit_icon.png[]). -. Enter a custom label for the field, if needed. -. Select a format and fill in the details. -+ -[role="screenshot"] -image:management/index-patterns/images/edit-field-format.png["Edit field format"] - - - -[[field-formatters-string]] -=== String field formatters - -String fields support the `String` and `Url` formatters. - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/url-formatter.asciidoc[] - -[[field-formatters-date]] -=== Date field formatters - -Date fields support the `Date`, `Url`, and `String` formatters. - -The `Date` formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] -standard format definitions. - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/url-formatter.asciidoc[] - -[[field-formatters-geopoint]] -=== Geographic point field formatters - -Geographic point fields support the `String` formatter. - -include::field-formatters/string-formatter.asciidoc[] - -[[field-formatters-numeric]] -=== Numeric field formatters - -Numeric fields support the `Url`, `Bytes`, `Duration`, `Number`, `Percentage`, `Histogram`, `String`, and `Color` formatters. - -The `Bytes`, `Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using -the <> syntax that {kib} maintains. - -The `Histogram` formatter is only used for the {ref}/histogram.html[histogram field type]. When using the `Histogram` formatter, -you can apply the `Number`, `Bytes`, or `Percentage` format to the aggregated data. - -`Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using -the <> syntax that {kib} maintains. - -include::field-formatters/url-formatter.asciidoc[] - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/duration-formatter.asciidoc[] - -include::field-formatters/color-formatter.asciidoc[] - -[[scripted-fields]] -=== Scripted fields -deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] - -Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on -the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default -query language. - -WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on -{kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are -buggy, you'll get exceptions whenever you try to view the dynamically generated data. - -When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the -{ref}/modules-scripting-painless.html[Painless] scripting language. - -You can reference any single value numeric field in your expressions, for example: - ----- -doc['field_name'].value ----- - -For more information on scripted fields and additional examples, refer to -https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] - -[float] -[[create-scripted-field]] -=== Create a scripted field - -. Open the main menu, then click *Stack Management > Index Patterns*. -. Select the index pattern you want to add a scripted field to. -. Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*. -. Enter a name for the scripted field. -. Enter the expression that you want to use to compute a value on the fly from your index data. -. Click *Create field*. - -For more information about scripted fields in {es}, see -{ref}/modules-scripting.html[Scripting]. - -[float] -[[update-scripted-field]] -=== Update a scripted field - -. Click the *Scripted fields* tab for the index pattern. -. Click the *Edit* button for the scripted field you want to change. -. Make your changes, then click *Save field*. - -WARNING: Built-in validation is unsupported for scripted fields. If your scripts are buggy, you'll get -exceptions whenever you try to view the dynamically generated data. - -[float] -[[delete-scripted-field]] -=== Delete a scripted field - -. Click the *Scripted fields* tab for the index pattern. -. Click *Delete* for the scripted field you want to remove. -. Click *Delete* on the confirmation window. diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index 0e1ed0b9e1bec5f..a58e8ac8902b8b0 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -26,7 +26,7 @@ image::maps/images/inspector.png[] * Verify your geospatial data is correctly mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. ** Run `GET myIndexPatternTitle/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexPatternTitle` and `myGeoFieldName` with your index pattern title and geospatial field name. ** Ensure response specifies `type` as `geo_point` or `geo_shape`. -* Verify your geospatial data is correctly mapped in your <>. +* Verify your geospatial data is correctly mapped in your <>. ** Open your index pattern in <>. ** Ensure your geospatial field type is `geo_point` or `geo_shape`. ** Ensure your geospatial field is searchable and aggregatable. diff --git a/docs/maps/vector-tooltips.asciidoc b/docs/maps/vector-tooltips.asciidoc index b0498c9088e4e68..2dda35aa28f768f 100644 --- a/docs/maps/vector-tooltips.asciidoc +++ b/docs/maps/vector-tooltips.asciidoc @@ -18,7 +18,7 @@ image::maps/images/multifeature_tooltip.png[] ==== Format tooltips You can format the attributes in a tooltip by adding <> to your -Kibana index pattern. You can use field formatters to round numbers, provide units, +index pattern. You can use field formatters to round numbers, provide units, and even display images in your tooltip. [float] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4aedb0f516b20bf..a14bda2bf5a9867 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -279,26 +279,32 @@ This content has moved. Refer to <>. [role="exclude",id="ingest-node-pipelines"] == Ingest Node Pipelines -This content has moved. See {ref}/ingest.html[Ingest pipelines]. +This content has moved. Refer to {ref}/ingest.html[Ingest pipelines]. [role="exclude",id="create-panels-with-timelion"] == Timelion -This content has moved. refer to <>. +This content has moved. Refer to <>. [role="exclude",id="space-rbac-tutorial"] == Tutorial: Use role-based access control to customize Kibana spaces -This content has moved. refer to <>. +This content has moved. Refer to <>. [role="exclude",id="search"] == Search your data -This content has moved. refer to <>. +This content has moved. Refer to <>. [role="exclude",id="discover-document-context"] == View surrounding documents -This content has moved. refer to <>. +This content has moved. Refer to <>. + +[role="exclude",id="field-formatters-string"] +== String field formatters + +This content has moved. Refer to <>. + diff --git a/docs/user/alerting/images/rule-details-alerts-inactive.png b/docs/user/alerting/images/rule-details-alerts-inactive.png index f84910ae0dcdc4b..fc82cf465ebb2da 100644 Binary files a/docs/user/alerting/images/rule-details-alerts-inactive.png and b/docs/user/alerting/images/rule-details-alerts-inactive.png differ diff --git a/docs/user/alerting/images/rules-imported-banner.png b/docs/user/alerting/images/rules-imported-banner.png new file mode 100644 index 000000000000000..54dd5205a488d0f Binary files /dev/null and b/docs/user/alerting/images/rules-imported-banner.png differ diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index b15c46254b770e3..e47858f58cd1a6c 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -62,6 +62,9 @@ image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, === Importing and exporting rules To import and export rules, use the <>. +After the succesful import the proper banner will be displayed: +[role="screenshot"] +image::images/rules-imported-banner.png[Rules import banner, width=50%] [float] === Required permissions diff --git a/docs/user/dashboard/images/lens_advanced_2_1.png b/docs/user/dashboard/images/lens_advanced_2_1.png index 5090f0d3b2841f4..dab32369d71a1d9 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_1.png and b/docs/user/dashboard/images/lens_advanced_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_1_1.png b/docs/user/dashboard/images/lens_advanced_2_1_1.png deleted file mode 100644 index f4d9ca488782e61..000000000000000 Binary files a/docs/user/dashboard/images/lens_advanced_2_1_1.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1.gif b/docs/user/dashboard/images/lens_advanced_2_2.gif similarity index 100% rename from docs/user/dashboard/images/lens_advanced_3_1.gif rename to docs/user/dashboard/images/lens_advanced_2_2.gif diff --git a/docs/user/dashboard/images/lens_advanced_2_2.png b/docs/user/dashboard/images/lens_advanced_2_2.png index 820bc3bd4dfa9e3..1d88bcd238ca302 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_2.png and b/docs/user/dashboard/images/lens_advanced_2_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_2_2_1.png b/docs/user/dashboard/images/lens_advanced_2_2_1.png index 3044f1070367d04..c3fb697666b4624 100644 Binary files a/docs/user/dashboard/images/lens_advanced_2_2_1.png and b/docs/user/dashboard/images/lens_advanced_2_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_3.png b/docs/user/dashboard/images/lens_advanced_2_3.png similarity index 100% rename from docs/user/dashboard/images/lens_advanced_3_3.png rename to docs/user/dashboard/images/lens_advanced_2_3.png diff --git a/docs/user/dashboard/images/lens_advanced_3_1.png b/docs/user/dashboard/images/lens_advanced_3_1.png new file mode 100644 index 000000000000000..1473b203924a3a5 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_3_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_3_1_1.png b/docs/user/dashboard/images/lens_advanced_3_1_1.png deleted file mode 100644 index c3fb697666b4624..000000000000000 Binary files a/docs/user/dashboard/images/lens_advanced_3_1_1.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_advanced_3_2.png b/docs/user/dashboard/images/lens_advanced_3_2.png index 20da2ed706dfd2a..15f2f0228a0fc60 100644 Binary files a/docs/user/dashboard/images/lens_advanced_3_2.png and b/docs/user/dashboard/images/lens_advanced_3_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_4_1.png b/docs/user/dashboard/images/lens_advanced_4_1.png index 43c8db213d482cd..50d1affa268dd49 100644 Binary files a/docs/user/dashboard/images/lens_advanced_4_1.png and b/docs/user/dashboard/images/lens_advanced_4_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_4_2.png b/docs/user/dashboard/images/lens_advanced_4_2.png deleted file mode 100644 index 4b3e98910e7b794..000000000000000 Binary files a/docs/user/dashboard/images/lens_advanced_4_2.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_advanced_5_1.png b/docs/user/dashboard/images/lens_advanced_5_1.png new file mode 100644 index 000000000000000..5090f0d3b2841f4 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_5_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_5_2.png b/docs/user/dashboard/images/lens_advanced_5_2.png new file mode 100644 index 000000000000000..820bc3bd4dfa9e3 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_5_2.png differ diff --git a/docs/user/dashboard/images/lens_advanced_5_2_1.png b/docs/user/dashboard/images/lens_advanced_5_2_1.png new file mode 100644 index 000000000000000..3044f1070367d04 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_5_2_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_6_1.png b/docs/user/dashboard/images/lens_advanced_6_1.png new file mode 100644 index 000000000000000..5d5cefa472a13f2 Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_6_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_7_1.png b/docs/user/dashboard/images/lens_advanced_7_1.png new file mode 100644 index 000000000000000..3d66d5d7e4579ea Binary files /dev/null and b/docs/user/dashboard/images/lens_advanced_7_1.png differ diff --git a/docs/user/dashboard/images/lens_advanced_result.png b/docs/user/dashboard/images/lens_advanced_result.png index 19963d87c8e1c5d..8cf087f936abd40 100644 Binary files a/docs/user/dashboard/images/lens_advanced_result.png and b/docs/user/dashboard/images/lens_advanced_result.png differ diff --git a/docs/user/dashboard/images/manage-runtime-field.gif b/docs/user/dashboard/images/manage-runtime-field.gif new file mode 100644 index 000000000000000..c6ecf0caf818012 Binary files /dev/null and b/docs/user/dashboard/images/manage-runtime-field.gif differ diff --git a/docs/user/dashboard/images/runtime-field-menu.png b/docs/user/dashboard/images/runtime-field-menu.png new file mode 100644 index 000000000000000..891de38bb68333c Binary files /dev/null and b/docs/user/dashboard/images/runtime-field-menu.png differ diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 6b090f6017f5d7e..e49db0c0d026dc3 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -1,11 +1,11 @@ [[create-a-dashboard-of-panels-with-ecommerce-data]] -== Tutorial: Create a dashboard of panels with ecommerce sales data +== Time series analysis with Lens -You collected sales data from your store, and you want to visualize and analyze the data on a dashboard. +The tutorial uses sample data from the perspective of a shop owner looking +at sales trends, but this type of dashboard works on any type of data. To create dashboard panels of the data, open the *Lens* visualization builder, then create the visualization panels that best display the data. - -When you've completed the tutorial, you'll have a dashboard that provides you with a complete overview of your ecommerce sales data. +Before using this tutorial, you should be familiar with the <>. [role="screenshot"] image::images/lens_advanced_result.png[Dashboard view] @@ -14,36 +14,52 @@ image::images/lens_advanced_result.png[Dashboard view] [[add-the-data-and-create-the-dashboard-advanced]] === Add the data and create the dashboard -To create visualizations of the data from your store, add the data set, then create the dashboard. +If you are working with your own data, you should already have an <>. +To install the sample sales data: . From the {kib} *Home* page, click *Try our sample data*. . From *Sample eCommerce orders*, click *Add data*. +Then create a new dashboard: + . Open the main menu, then click *Dashboard*. . On the *Dashboards* page, click *Create dashboard*. +. Set the <> to *Last 30 days*. + [float] [[open-and-set-up-lens-advanced]] === Open and set up Lens -Open the *Lens* editor, then make sure the correct fields appear. +*Lens* is designed to help you quickly build visualizations for your dashboard, as shown in <>, while providing support for advanced usage as well. -. From the dashboard, click *Create panel*. +Open the *Lens* editor, then make sure the correct fields appear. -. On the *New visualization* window, click *Lens*. -+ -[role="screenshot"] -image::images/lens_end_to_end_1_1.png[New visualization popover] +. From the dashboard, click *Create visualization*. -. Make sure the *kibana_sample_data_ecommerce_* index appears. +. Make sure the *kibana_sample_data_ecommerce* index appears. [discrete] -[[view-the-number-of-transactions-per-day]] -=== View the number of transactions per hour +[[custom-time-interval]] +=== View a date histogram with a custom time interval + +It is common to use the automatic date histogram interval, but sometimes you want a larger or smaller +interval. *Lens* only lets you choose the minimum time interval, not the exact time interval, for +performance reasons. The performance limit is controlled by the <> +advanced setting and the overall time range. To see hourly sales over a 30 day time period, choose +one of these options: -To determine the number of orders made every hour, create a bar chart, then add the chart to the dashboard. +* View less than 30 days at a time, then use the time picker to select each day separately. + +* Increase `histogram:maxBars` from 100 to at least 720, which the number of hours in 30 days. +This affects all visualizations and can reduce performance. + +* If approximation is okay, use the *Normalize unit* option. This can convert *Average sales per 12 hours* +into *Average sales per 12 hours (per hour)* by dividing the number of hours. + +For the sample data, approximation is okay. To use the *Normalize unit* option: . Set the <> to *Last 30 days*. @@ -75,183 +91,258 @@ image::images/lens_advanced_1_2.png[Orders per day] . Click *Save and return*. [discrete] -[[view-the-cumulative-number-of-products-sold-over-time]] -=== View the cumulative number of products sold on weekends +[[add-a-data-layer-advanced]] +=== Monitor multiple series within a date histogram -To determine the number of orders made only on Saturday and Sunday, create an area chart, then add it to the dashboard. +It is often required to monitor multiple series within a time interval. These series can be have similar configurations with few changes between one and another. +*Lens* copies a function when you drag and drop it to the *Drop a field or click to add* +field within the same group, or when you drag and drop to the *Duplicate* field on a different group. +You can also drag and drop using your keyboard. For more information, refer to <>. -. Open *Lens*. +To quickly create many copies of a percentile metric that shows distribution of price over time: -. From the *Chart Type* dropdown, select *Area*. +. From the *Chart Type* dropdown, select *Line*. + [role="screenshot"] -image::images/lens_advanced_2_1_1.png[Chart type menu with Area selected] +image::images/lens_advanced_2_1.png[Chart type menu with Line selected] -. Configure the cumulative sum of the store orders. - -.. From the *Available fields* list, drag and drop *Records* to the visualization builder. +. From the *Available fields* list, drag and drop *products.price* to the visualization builder. -.. From the editor, click *Count of Records*. +. Create the 95th percentile. -.. From *Select a function*, click *Cumulative sum*. +.. In the editor, click *Median of products.price*. -.. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. +.. From *Select a function*, click *Percentile*. -. Filter the results to display the data for only Saturday and Sunday. +.. In the *Display name* field, enter `95th`, then click *Close*. -.. From the editor, click the *Drop a field or click to add* field for *Break down by*. +. To create the 90th percentile, duplicate the `95th` percentile. -.. From *Select a function*, click *Filters*. +.. Drag and drop *95th* to *Drop a field or click to add*. -.. Click *All records*. +.. Click *95th [1]*, then enter `90` in the *Percentile* field. -.. In the *KQL* field, enter `day_of_week : "Saturday" or day_of_week : "Sunday"`, then press Return. -+ -The <> displays all documents where `day_of_week` matches `Saturday` or `Sunday`. +.. In the *Display name* field enter `90th`, then click *Close*. + [role="screenshot"] -image::images/lens_advanced_2_1.png[Filter aggregation to filter weekend days] +image::images/lens_advanced_2_2.gif[Easily duplicate the items with drag and drop] -. To hide the legend, open the *Legend* menu, then click *Hide*. +.. Repeat the duplication steps to create the `50th` and `10th` percentile, naming them accordingly. + +. To change the left axis label, open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. + [role="screenshot"] -image::images/lens_advanced_2_2_1.png[Legend menu] +image::images/lens_advanced_2_2_1.png[Left Axis menu] + -You have an area chart that shows you how many orders your store received during the weekend. +You have a line chart that shows you the price distribution of products sold over time. + [role="screenshot"] -image::images/lens_advanced_2_2.png[Line chart with cumulative sum of orders made on the weekend] +image::images/lens_advanced_2_3.png[Percentiles for product prices chart] -. Click *Save and return*. +. Add the filter for the redirect codes. [discrete] -[[add-a-data-layer-advanced]] -=== Create multiple key percentiles of product prices - -To view the price distribution of products sold over time, create a percentile chart, then add it to the dashboard. - -. Open *Lens*. - -. From the *Chart Type* dropdown, select *Line*. - -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. - -* Drag and drop *products.price* to the *Vertical axis* field. +[[add-a-data-layer]] +==== Multiple chart types or index patterns in one visualization -* Drag and drop *order_date* to the *Horizontal axis* field. +You can add multiple metrics to a single chart type, but if you want to overlay +multiple chart types or index patterns, use a second layer. When building layered charts, +it is important to match the data on the horizontal axis so that it uses the same +scale. To add a line chart layer on top of an existing chart: -. Create the 95th percentile. +To compare product prices with customers traffic: -.. In the editor, click *Median of products.price*. +. From the *Available fields* list, drag and drop *products.price* to the visualization builder. -.. From *Select a function*, click *Percentile*. +.. In the *KQL* field, enter `response.keyword>=500 AND response.keyword<600`. -.. In the *Display name* field, enter `95th`, then click *Close*. +.. From *Select a function*, click *Average*. -. To create the 90th percentile, duplicate the `95th` percentile. +.. In the *Display name* field, enter `Average of prices`, then click *Close*. -.. Drag and drop *95th* to *Drop a field or click to add*. +. From the *Chart Type* dropdown, select *Area*. -.. Click *95th [1]*, then enter `90` in the *Percentile* field. +. Create a new layer to overlay with custom traffic. -.. In the *Display name* field enter `90th`, then click *Close*. +. In the editor, click *+*. + [role="screenshot"] -image::images/lens_advanced_3_1.gif[Easily duplicate the items with drag and drop] - -. Create the 50th percentile. - -.. Drag and drop *90th* to *Drop a field or click to add*. +image::images/lens_advanced_3_1.png[Add new layer button] -.. Click *90th [1]*, then enter `50` in the *Percentile* field. +. From the *Available fields* list, drag and drop *customer_id* to the *Vertical Axis* of the newly created layer. -.. In the *Display name* field enter `50th`, then click *Close*. +.. In the editor, click *Unique count of customer_id*. -. Create the 10th percentile. +.. In the *Display name* field, enter `Unique customers`, then click *Close*. -.. Drag and drop *50th* to *Drop a field or click to add*. +. In the *Series color* field, enter *#D36086*, then click *Close*. -.. Click *50th [1]*, then enter `10` in the *Percentile* field. +. For *Axis side*, click *Right*, then click *Close*. -.. In the *Display name* field enter `10th`, then click *Close*. +. From the *Available fields* list, drag and drop *order_date* to the *Horizontal Axis* of the newly created layer. -. To change the left axis label, open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. +. From the new layer editor, click the *Chart type* dropdown, then click the line chart. + [role="screenshot"] -image::images/lens_advanced_3_1_1.png[Left Axis menu] -+ -You have a line chart that shows you the price distribution of products sold over time. -+ -[role="screenshot"] -image::images/lens_advanced_3_3.png[Percentiles for product prices chart] +image::images/lens_advanced_3_2.png[Change layer type] + +The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart. + +. From the *Legend* dropdown, select the top position. . Click *Save and return*. [discrete] -[[add-the-response-code-filters-advanced]] -=== View the moving average of inventory prices +[[percentage-stacked-area]] +=== Compare the change in percentage over time -To view and analyze the prices of shoes, accessories, and clothing in the store inventory, create a line chart. +By default, *Lens* shows *date histograms* using a stacked chart visualization, which helps understand how distinct sets of documents perform over time. Sometimes it is useful to understand how the distributions of these sets change over time. +Combine *filters* and *date histogram* functions to see the change over time in specific +sets of documents. To view this as a percentage, use a *stacked percentage* bar or area chart. -. Open *Lens*. +To see sales change of product by type over time: -. From the *Chart Type* dropdown, select *Line*. +. From the *Available fields* list, drag and drop *Records* to the visualization builder. -. From the *Available fields* list, drag and drop *products.price* to the visualization builder. +. Click *Bar vertical stacked*, then select *Area percentage*. -. In the editor, click the *Drop a field or click to add* field for *Break down by*. +For each category type that you want to break down, create a filter. + +. In the editor, click the *Drop a field or click to add* field for *Break down by*. . From *Select a function*, click *Filters*. -. Add a filter for shoes. +. Add the filter for the clothing category. .. Click *All records*. -.. In the *KQL* field, enter `category.keyword : *Shoes*`. +.. In the *KQL* field, enter `category.keyword : *Clothing`. + +.. In the *Label* field, enter `Clothing`, then press Return. + +. Add the filter for the shoes category. + +.. Click *Add a filter*. + +.. In the *KQL* field, enter `category.keyword : *Shoes`. .. In the *Label* field, enter `Shoes`, then press Return. -. Add a filter for accessories. +. Add the filter for the accessories category. .. Click *Add a filter*. -.. In the *KQL* field, enter `category.keyword : *Accessories*`. +.. In the *KQL* field, enter `category.keyword : *Accessories`. .. In the *Label* field, enter `Accessories`, then press Return. -. Add a filter for clothing. +Change the legend position to the top of the chart. -.. Click *Add a filter*. +. From the *Legend* dropdown, select the top position. -.. In the *KQL* field, enter `category.keyword : *Clothing*`. ++ +[role="screenshot"] +image::images/lens_advanced_4_1.png[Prices share by category] -.. In the *Label* field, enter `Clothing`, then press Return. + Click *Save and return*. + +[discrete] +[[view-the-cumulative-number-of-products-sold-on-weekends]] +=== View the cumulative number of products sold on weekends + +To determine the number of orders made only on Saturday and Sunday, create an area chart, then add it to the dashboard. + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Area*. + +. Configure the cumulative sum of the store orders. + +.. From the *Available fields* list, drag and drop *Records* to the visualization builder. + +.. From the editor, click *Count of Records*. + +.. From *Select a function*, click *Cumulative sum*. + +.. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. + +. Filter the results to display the data for only Saturday and Sunday. + +.. From the editor, click the *Drop a field or click to add* field for *Break down by*. + +.. From *Select a function*, click *Filters*. + +.. Click *All records*. + +.. In the *KQL* field, enter `day_of_week : "Saturday" or day_of_week : "Sunday"`, then press Return. ++ +The <> displays all documents where `day_of_week` matches `Saturday` or `Sunday`. ++ +[role="screenshot"] +image::images/lens_advanced_5_1.png[Filter aggregation to filter weekend days] + +. To hide the legend, open the *Legend* menu, then click *Hide*. ++ +[role="screenshot"] +image::images/lens_advanced_5_2_1.png[Legend menu] ++ +You have an area chart that shows you how many orders your store received during the weekend. -. Click *Close* +. Click *Bar vertical stacked*, then select *Area*. + [role="screenshot"] -image::images/lens_advanced_4_1.png[Median prices chart for different categories] +image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders made on the weekend] + +. Click *Save and return*. [discrete] -[[add-the-moving-average]] -==== Add the moving average +[[view-customers-over-time-by-continents]] +=== View table of customers by category over time + +Tables are an alternative type of visualization for time series, useful when you want to read the actual values. +You can build a date histogram table, and group the customer count metric by category, like the continent registered in their profile. + +In *Lens* you can split the metric in a table leveraging the *Columns* field, where each data value from the aggregation is used as column of the table and the relative metric value is shown. + +To build a date histogram table: + +. Open *Lens*. + +. From the *Chart Type* dropdown, select *Table*. -To focus on the general trends rather than on the peaks in the data, add the moving average, then add the visualization to the dashboard. +.. From the *Available fields* list, drag and drop *customer_id* to the *Metrics* field of the editor. -. In the editor, click the *Median of products.price*. +.. From the editor, click *Unique count of customer_id*. -. From *Select a function*, click *Moving average*. +.. In the *Display name* field, enter `Customers`, then click *Close*. -. In the *Window size* field, enter `7`, then click *Close*. +.. From the *Available fields* list, drag and drop *order_date* to the *Rows* field of the editor. + +.. From the editor *Rows*, click the *order_date* field just dropped. + +. Select *Customize time interval*. + +. Change the *Minimum interval* to `1 days`, then click *Close*. + +.. In the *Display name* field, enter `Sale`, then click *Close*. + +To split the customers count by continent: + +. From the *Available fields* list, drag and drop *geoip.continent_name* to the *Columns* field of the editor. + [role="screenshot"] -image::images/lens_advanced_4_2.png[Moving average prices chart for different categories] +image::images/lens_advanced_6_1.png[Table with daily customers by continent configuration] . Click *Save and return*. [discrete] === Save the dashboard +By default the dashboard attempts to match the palette across panels, but in this case there's no need for that, so it can be disabled. + +[role="screenshot"] +image::images/lens_advanced_7_1.png[Disable palette sync in dashboard] + Now that you have a complete overview of your ecommerce sales data, save the dashboard. . In the toolbar, click *Save*. diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_1.png deleted file mode 100644 index 1c752972791af6f..000000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png index de719c852dfdef2..ea58057433a19eb 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png index 9408cd736b27eea..fa404d50b4dd1f5 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_2_1.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png index 4302c95eb8a2512..b069acae43163ed 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_1_3.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1.png deleted file mode 100644 index c08a865e52137f4..000000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png index 48712ec5ef224e4..e996b58520d410d 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_1.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_2.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_2.png new file mode 100644 index 000000000000000..a524dd1e456f6f0 Binary files /dev/null and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_2_1_2.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png index b70910fdd2500c0..2cd75d2797a981a 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_4_3.png differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_6_2.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_6_2.png deleted file mode 100644 index e95b3d9a97b8d4f..000000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_6_2.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_7_1.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_7_1.png deleted file mode 100644 index e63f7e77f1e9f62..000000000000000 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_7_1.png and /dev/null differ diff --git a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png index 9bce65401f4c966..9e9a5ed3d758eb9 100644 Binary files a/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png and b/docs/user/dashboard/lens-end-to-end/images/lens_end_to_end_dashboard.png differ diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 58476bcae87dffc..94c9db1462760f9 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -75,6 +75,31 @@ Drag and drop the fields on to the visualization builder, then [role="screenshot"] image::images/lens_value_labels_xychart_toggle.png[Lens Bar chart value labels menu] +[float] +[[add-fields-in-lens]] +===== Add fields + +Add and define fields to the index pattern that you want to visualize using the {ref}/modules-scripting-painless.html[Painless scripting language]. +The fields that you are add are saved to the index pattern and appear in all visualizations, saved searches, and saved objects that use the index pattern. + +. Click *...*, then select *Add field to index pattern*. ++ +[role="screenshot"] +image:images/runtime-field-menu.png[Dropdown menu located next to index pattern field with items for adding and managing fields, width=50%] + +. Enter a *Name* for the field, then select the field *Type*. + +. Select *Set value*, then define the field value by emitting a single value using the {ref}/modules-scripting-painless.html[Painless scripting language]. + +. Click *Save*. ++ +To manage the field, click the field, then click *Edit index pattern field* or *Remove index pattern field*. ++ +[role="screenshot"] +image:images/manage-runtime-field.gif[Field menu to edit or remove field from index pattern, width=50%] + +For more information about adding fields to index patterns and Painless scripting language examples, refer to <>. + [float] [[drag-and-drop-keyboard-navigation]] ===== Create visualization panels with keyboard navigation diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index ff71cd7b383bdc7..12d0169c13f661b 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -4,7 +4,7 @@ Instead of using a visual editor to create charts, you define a graph by chaining functions together, using the *Timelion*-specific syntax. The syntax enables some features that classical point series charts don't offer, such as pulling data from different indices or data sources into one graph. -deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In 8.0 and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] +deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In the last 7.x minor version and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] [float] ==== Timelion expressions @@ -554,4 +554,4 @@ Save and add the panel to the dashboard. . Click *Save and return*. -For more information about *Timelion* conditions, refer to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. \ No newline at end of file +For more information about *Timelion* conditions, refer to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index ff1c16c14d46715..5c4ce8e365e865f 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -2,7 +2,8 @@ === TSVB *TSVB* enables you to visualize the data from multiple data series, supports <>, multiple visualization types, custom functions, and some math. To use *TSVB*, your data must have a date field. +most {es} metric aggregations>>, multiple visualization types, custom functions, and some math. +To create *TSVB* visualization panels, your data must have a time field. [role="screenshot"] image::visualize/images/tsvb-screenshot.png[TSVB overview] @@ -17,15 +18,19 @@ Open *TSVB*, then make sure the required settings are configured. . On the *New visualization* window, click *TSVB*. -. In *TSVB*, click *Panel options*, then make sure the following settings are configured: +. In *TSVB*, click *Panel options*, then specify the required *Data* settings. -* *Index pattern* -* *Time field* -* *Interval* +.. From the *Index pattern* dropdown, select the index pattern you want to visualize. ++ +To visualize an {es} index, open the *Index pattern select mode* menu, deselect *Use only {kib} index patterns*, then enter the {es} index. + +.. From the *Time field* dropdown, select the field you want to visualize, then enter the field *Interval*. -. Select a *Drop last bucket* option. It is dropped by default because the time filter intersects the time range of the last bucket, but can be enabled to see the partial data. +.. Select a *Drop last bucket* option. ++ +By default, *TSVB* drops the last bucket because the time filter intersects the time range of the last bucket. To view the partial data, select *No*. -. In the *Panel filter* field, specify any <> to select specific documents. +.. In the *Panel filter* field, enter <> to view specific documents. [float] [[configure-the-data-series]] diff --git a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index 22483b28018488c..1375aa8d5934e73 100644 --- a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -1,10 +1,10 @@ [[create-a-dashboard-of-panels-with-web-server-data]] -== Tutorial: Create a dashboard of panels with web server data +== Build your first dashboard -You collected data from your web server, and you want to visualize and analyze the data on a dashboard. To create dashboard panels of the data, open the *Lens* visualization builder, then -create the visualization panels that best display the data. - -When you've completed the tutorial, you'll have a dashboard that provides you with a complete overview of your web server data. +Learn the most common ways to build a dashboard from your own data. +The tutorial will use sample data from the perspective of an analyst looking +at website logs, but this type of dashboard works on any type of data. +Before using this tutorial, you should be familiar with the <>. [role="screenshot"] image::images/lens_end_to_end_dashboard.png[Final dashboard vis] @@ -13,46 +13,78 @@ image::images/lens_end_to_end_dashboard.png[Final dashboard vis] [[add-the-data-and-create-the-dashboard]] === Add the data and create the dashboard -To create visualizations of the data from the web server, add the data set, then create the dashboard. +Install the sample web logs data that you'll use to create your dashboard. -. From the {kib} *Home* page, click *Try our sample data*. +. On the {kib} *Home* page, click *Try our sample data*. . From *Sample web logs*, click *Add data*. -. Open the main menu, click *Dashboard*. +Then create a new dashboard: + +. Open the main menu, then click *Dashboard*. . Click *Create dashboard*. +. Set the <> to *Last 90 days*. + [float] [[open-and-set-up-lens]] -=== Open and set up Lens +=== Open Lens and get familiar with the data -With *Lens*, you identify the data fields you want to visualize, drag and drop the fields, then watch as -*Lens* uses heuristics to apply the fields and create a visualization for you. +. On the dashboard, click *Create visualization*. -. From the dashboard, click *Create panel*. - -. On the *New visualization* window, click *Lens*. +. Make sure the *kibana_sample_data_logs* index appears. You might need to select +a different index pattern from the dropdown: + [role="screenshot"] -image::images/lens_end_to_end_1_1.png[New visualization popover] +image::images/lens_end_to_end_1_2.png[Lens index pattern selector, width=50%] + +This tutorial uses `Records`, timestamp`, `bytes`, `clientip`, and `referer.keyword`. +To see the most frequent values of a field, click the field name to view a summary. + +The main elements of *Lens* are named: -. Make sure the *kibana_sample_data_logs* index appears. +Workspace panel:: Displays your visualization. You can drag and drop onto this area. +Dimensions:: Each dimension is a function with extra options. Dimensions are grouped +for each visualization type, for example the *Vertical axis* is a group that allows +multiple dimensions. Each dimension starts empty with the label *Drop a field or click to add*. +Functions:: There are two main types of functions: *buckets* and *metrics*, which are +equivalent to what {es} provides. [discrete] [[view-the-number-of-website-visitors]] -=== View the number of website visitors +=== Create your first visualization -To determine how many users have visited your website within the last 90 days, create a metric visualization, then add it to the dashboard. +Every time you build a visualization in *Lens*, you need to: -. Set the <> to *Last 90 days*. +* *Choose your visualization.* Do you know the type of visualization you'd like to use? +If you do, select the type before dragging any fields. If you don't, you can change the +visualization type after configuring your functions. + +* *Choose your field.* Do you know the dimension group you want to use the field in? If you do, +drag and drop the field from the field list to your chosen dimension and Lens will pick a function for you. +If you don't, drag and drop the field onto the workspace panel. Skip this step if you are +using the *Filters* function. + +* *Edit and delete.* To change the function or styling options, click the dimension to open +the configuration panel. To delete a specific dimension, close the configuration panel and click +the delete button. To reset the entire visualization, click *Reset layer*. + +To put this into practice, pick a field you want to analyze, such as `clientip`. If you want +to analyze only this field, you can use *Metric* to show a big number. +The only number function that you can use with `clientip` is *Unique count*. +*Unique count*, also known as cardinality, approximates the number of unique values +of the `clientip` field. -. From the *Chart Type* dropdown, select *Metric*. +. To select the visualization type, click *Bar vertical stacked* to open the chart type dropdown, then select *Metric*. + [role="screenshot"] -image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected] +image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected, width=50%] -. From the *Available fields* list, drag and drop *clientip* to the visualization builder. +. From the *Available fields* list, drag and drop `clientip` to the workspace panel. +Lens selects *Unique count* because it is the only numeric function +that works for IP addresses. You can also drag and drop `clientip` onto +the empty dimension for the same result. + [role="screenshot"] image::images/lens_end_to_end_1_3.png[Changed type and dropped clientip field] @@ -68,294 +100,220 @@ image::images/lens_end_to_end_1_4.png[Flyout config open] . Click *Save and return*. -[discrete] -[[view-the-distribution-of-visitors-by-operating-system]] -=== View the distribution of visitors by operating system - -To determine the operating systems you should continue to support, and the importance of mobile traffic from iOS devices, -create a donut chart that displays the top operating systems that your visitors used to access your website within the last 90 days. +. Customize the newly added panel: -. Open *Lens*, then set the <> to *Last 90 days*. +.. Drag the bottom corner of the panel until the metric takes up one quarter of the screen +width. The row for the metric will have 4 items on it later. -. From the *Chart Type* dropdown, select *Donut*. +.. The metric visualization has its own label, so you do not need to add a panel title. -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. +. Click *Save* on the dashboard menu -.. Drag and drop *clientip* to the *Size by* field. +. In the *Title* field, enter `Logs dashboard`. -.. Drag and drop *machine.os.keyword* to the *Slice by* field. -+ -[role="screenshot"] -image::images/lens_end_to_end_2_1_1.png[Donut chart with clientip and machine.os.keyword fields] - -. Change the color palette. - -.. In the editor, click *Top values of machine.os.keyword*. - -.. From the *Color palette* dropdown, select *Compatibility*. - -.. Click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_2_1.png[Donut chart with open config panel] +. Select *Store time with dashboard* box, then click *Save*. -. Click *Save and return*. +. After the dashboard refreshes, click *Edit* again. [discrete] [[mixed-multiaxis]] -=== View the average of bytes transfer per day +=== View a metric over time -To prevent potential server failures, and optimize the cost of website maintenance, create an area chart that displays the average of bytes transfer. To compare -the data to the number of visitors to your website, add a line chart layer. +*Lens* has two shortcuts that simplify viewing metrics over time. +If you drag and drop a numeric field to the workspace panel, *Lens* adds the default +time field from the index pattern. If the *Date histogram* function is being used, +quickly replace the time field by dragging and dropping on the workspace panel. -. Open *Lens*. +To visualize the `bytes` field over time without choosing a visualization type or function: -. From the *Available fields* list, drag and drop *bytes* to the visualization builder. +. From the *Available fields* list, drag and drop `bytes` onto the workspace panel to have *Lens* automatically +create a chart. *Lens* creates a bar chart with two dimensions, *timestamp* and *Median of bytes*. + +. *Lens* automatically chooses a date interval. To zoom in on the data you want to view, +click and drag your cursor across the bars. -. To zoom in on the data you want to view, click and drag your cursor across the bars. -+ [role="screenshot"] image::images/lens_end_to_end_3_1_1.gif[Zoom in on the data] -. Change the *timestamp* interval. - -.. In the editor, click *timestamp*. +To emphasize the change in *Median of bytes* over time, use a line chart. +To change the visualization type, use one of the following ways: -.. Select *Customize time interval*. +* From the *Suggestions*, click the line chart. +* Click *Bar vertical stacked*, then select *Line*. +* Click the chart type icon above *Horizontal axis*, then click the line icon. -.. Change the *Minimum interval* to `1 days`, then click *Close*. -+ -[role="screenshot"] -image::images/lens_end_to_end_3_1.png[Customize time interval] +Most users use the automatic time interval. You can increase and decrease +the minimum interval that *Lens* uses, but you cannot decrease the interval +below the {kib} advanced settings. To set the minimum time interval: -. From the *Chart Type* dropdown, select *Area*. +. In the editor, click *timestamp*. -[discrete] -[[add-a-data-layer]] -==== Add the line chart layer +. Click *How it works* to learn about the *Lens* minimum interval -To compare the average of bytes transfer to the number of users that visit your website, add a line chart layer. +. Select *Customize time interval*. -. In the editor, click *+*. +. Increase the *Minimum interval* to `1 days`, then click *Close*. + [role="screenshot"] -image::images/lens_end_to_end_3_2.png[Add new layer button] +image::images/lens_end_to_end_3_1.png[Customize time interval] -. From the new layer editor, click the *Chart type* dropdown, then click the line chart. +To save space on the dashboard, so to save space, hide the vertical and horizontal +axis labels. + +. Open the *Left axis* menu, then deselect *Show*. + [role="screenshot"] -image::images/lens_end_to_end_3_3.png[Change layer type] -+ -The chart type for the visualization changes to *Mixed XY*. - -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. - -.. Drag and drop *timestamp* to the *Horizontal axis* field. - -.. Drag and drop *clientip* to the *Vertical axis* field. - -. Change the *timestamp* interval. +image::images/lens_end_to_end_4_3.png[Turn off axis name] -.. In the editor, click *timestamp* in the line chart layer. +. Open the *Bottom axis* menu, then deselect *Show*. -.. Select *Customize time interval*. +. Click *Save and return* -.. Change the *Minimum interval* to `1 days`, then click *Close*. +. On the dashboard, move the panel so that it is in the same row as the *Metric* visualization panel. The two should +take up half the screen width. -. Change the *Unique count of clientip* label and color. +. Add a panel title to explain the panel, which is necessary because you removed the axis labels. -.. In the editor, click *Unique count of clientip*. +.. Open the panel menu and choose *Edit panel title*. -.. In the *Display name* field, enter `Unique visitors` in the line chart layer. +.. In the *Title* field, enter `Median of bytes`, then click *Save*. -.. In the *Series color* field, enter *#CA8EAE*, then click *Close*. +. In the toolbar, click *Save*. [discrete] -[[configure-the-multiaxis-chart]] -==== Configure the y-axes - -There is a significant difference between the *timestamp per day* and *Unique visitors* data, which makes the *Unique visitors* data difficult to read. To improve the readability, -display the *Unique visitors* data along a second y-axis, then change the formatting. When functions contain multiple formats, separate axes are created by default. +[[view-the-distribution-of-visitors-by-operating-system]] +=== View the top values of a field -. In the editor, click *Unique visitors* in the line chart layer. +The *Top values* function ranks the unique values of a field by another dimension. +The values are the most frequent when ranked by a *Count* dimension. +The values are the largest when ranked by a *Sum* dimension. -. For *Axis side*, click *Right*, then click *Close*. +When you drag and drop a text or IP address field onto the workspace panel, +*Lens* adds a *Top values* function ranked by *Count of records* to show the most frequent values. -[float] -[[change-the-visualization-type]] -==== Change the visualization type +For this tutorial, you have picked a field and function, but not a visualization type. +You want to see the most frequent values of `request.keyword` on your website, ranked by the unique visitors. +This means that you want to use *Top values of request.keyword* ranked by *Unique count of clientip*, instead of +being ranked by *Count of records*. -. In the editor, click *Average of bytes* in the area chart layer. +. From the *Available fields* list, drag and drop `clientip` onto the *Vertical axis*. +*Lens* chooses the function for you when you drop onto a dimension, which is *Unique count* here. +Do not drop the field into the main workspace because `clientip` will be added to the wrong axis. -. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. +. Drag and drop `request.keyword` to the main workspace. *Lens* adds *Top values of request.keyword* +to the *Horizontal axis*. + [role="screenshot"] -image::images/lens_end_to_end_3_4.png[Multiaxis chart] - -[discrete] -[[lens-legend-position]] -==== Change the legend position +image::images/lens_end_to_end_2_1_1.png[Vertical bar chart with top values of request.keyword by most unique visitors] -The visualization is done, but the legend uses a lot of space. Change the legend position to the top of the chart. +This chart is hard to read because the `request.keyword` field contains long text. You could try +using one of the *Suggestions*, but the suggestions also have issues with long text. Instead, switch +to the *Table* visualization. -. From the *Legend* dropdown, select the top position. +Click *Bar vertical stacked*, then select *Table*. + [role="screenshot"] -image::images/lens_end_to_end_3_5.png[legend position] - -. Click *Save and return*. - -[discrete] -[[percentage-stacked-area]] -=== View the health of your website +image::images/lens_end_to_end_2_1_2.png[Table with top values of request.keyword by most unique visitors] -To detect unusual traffic, bad website links, and server errors, create a percentage stacked area chart that displays the associated response codes. +Next, customize the table. -. Open *Lens*. +. Click the *Top values of request.keyword* dimension. -. From the *Available fields* list, drag and drop the data fields to the *Drop a field or click to add* fields in the editor. +.. Increase the *Number of values*. The maximum allowed value is 1000. -.. Drag and drop *Records* to the *Vertical axis* field. +.. In the *Display name* field, enter `Page URL`, then click *Close*. -.. Drag and drop *@timestamp* to the *Horizontal axis* field. +. Click *Save and return*. -. From the *Chart Type* dropdown, select *Percentage bar*. +. Move the table panel so that it has its own row, but do not change the size. -. To remove the vertical axis label, click *Left axis*, then deselect *Show*. -+ -[role="screenshot"] -image::images/lens_end_to_end_4_3.png[Turn off axis name] +NOTE: You do not need a panel title because the table columns are clearly labeled. [discrete] -[[add-the-response-code-filters]] -==== Add the response code filters - -For each response code that you want to display, create a filter. - -. In the editor, click the *Drop a field or click to add* field for *Break down by*. - -. From *Select a function*, click *Filters*. - -. Add the filter for the successful response codes. +[[custom-ranges]] +=== Compare a subset of documents to all documents -.. Click *All records*. +To compare a field on subset of documents to all documents, you need to select two or more sets of documents that add up to 100%. +For this example, we are comparing documents where the `bytes` field is under 10 Kb to documents where `bytes` is over 10 Kb, +which are two sets that do not overlap. -.. In the *KQL* field, enter `response.keyword>=200 AND response.keyword<300`. +Use *Intervals* to select documents based on the number range of a field. Use *Filters* when your criteria +is not numeric, or when your query needs multiple clauses. -.. In the *Label* field, enter `2XX`, then press Return. -+ -[role="screenshot"] -image::images/lens_end_to_end_4_1.png[First filter in filters aggregation] +Use a proportion chart to display the values as a percentage of the sum of all values. Lens has 5 types of proportion charts: +pie, donut, treemap, percentage bar and percentage area. -. Add the filter for the redirect codes. +To determine if your users transfer more `bytes` from small files versus large files, +configure dimensions with *Intervals* and *Sum*, then switch to a pie chart to display as a percentage: -.. Click *Add a filter*. +. From the *Available fields* list, drag and drop `bytes` to *Vertical axis* in the editor. -.. In the *KQL* field, enter `response.keyword>=300 AND response.keyword<400`. +. Click *Median of bytes*, select *Sum*, then click *Close*. -.. In the *Label* field, enter `3XX`, then press Return. +. From the *Available fields* list, drag and drop `bytes` to *Break down by* in the editor, then specify the file size ranges. -. Add the filter for the client error codes. +.. In the editor, click *bytes*. -.. Click *Add a filter*. +.. Click *Create custom ranges*, enter the following, then press Return: -.. In the *KQL* field, enter `response.keyword>=400 AND response.keyword<500`. +* *Ranges* — `0` -> `10240` -.. In the *Label* field, enter `4XX`, then press Return. +* *Label* — `Below 10KB` -. Add the filter for the server error codes. +.. Click *Add range*, enter the following, then press Return: -.. Click *Add a filter*. +* *Ranges* — `10240` -> `+∞` -.. In the *KQL* field, enter `response.keyword>=500 AND response.keyword<600`. +* *Label* — `Above 10KB` ++ +[role="screenshot"] +image::images/lens_end_to_end_6_1.png[Custom ranges configuration] -.. In the *Label* field, enter `5XX`, then press Return. +.. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. -. To change the color palette, select *Status* from the *Color palette* dropdown, then click *Close*. +. From the *Chart Type* dropdown, select *Pie*. . Click *Save and return*. [discrete] [[histogram]] -=== View the traffic for your website by the hour - -To find the best time to shut down your website for maintenance, create a histogram that displays the traffic for your website by the hour. - -. Open *Lens*. - -. From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. +=== View a the distribution of a number field -.. Click *Average of bytes*. +Knowing the distribution of a number helps to find patterns. For example, you could +look at the website traffic per hour to find the best time to do routine maintenance. +Use *Intervals* to see an evenly spaced distribution of a number field. -.. From *Select a function*, click *Sum*. +. From the *Available fields* list, drag and drop `bytes` to *Vertical axis* in the editor. -.. In the *Display name* field, enter `Transferred bytes`. +. Click *Median of bytes*, then select *Sum*. -.. From the *Value format* dropdown, select `Bytes (1024)`, then click *Close*. +. In the *Display name* field, enter `Transferred bytes`. -. From the *Available fields* list, drag and drop *hour_of_day* to *Horizontal axis* in the editor, then configure the options. +. From the *Value format* dropdown, select `Bytes (1024)`, then click *Close*. -.. Click *hour_of_day*. +. From the *Available fields* list, drag and drop *hour_of_day* to *Horizontal axis* in the editor. -.. Click and slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. +. Click *hour_of_day*, and then slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. + [role="screenshot"] image::images/lens_end_to_end_5_2.png[Create custom ranges] . Click *Save and return*. -[discrete] -[[custom-ranges]] -=== View the percent of small versus large transferred files - -To determine if your users transfer more small files versus large files, create a pie chart that displays the percentage of each size. - -. Open *Lens*. - -. From the *Available fields* list, drag and drop *bytes* to *Vertical axis* in the editor, then configure the options. - -.. Click *Average of bytes*. - -.. From *Select a function*, click *Sum*, then click *Close*. - -. From the *Available fields* list, drag and drop *bytes* to *Break down by* in the editor, then specify the file size ranges. - -.. Click *bytes*. - -.. Click *Create custom ranges*, enter the following, then press Return: - -* *Ranges* — `0` -> `10240` - -* *Label* — `Below 10KB` - -.. Click *Add range*, enter the following, then press Return: - -* *Ranges* — `10240` -> `+∞` - -* *Label* — `Above 10KB` -+ -[role="screenshot"] -image::images/lens_end_to_end_6_1.png[Custom ranges configuration] - -.. From the *Value format* dropdown, select *Bytes (1024)*, then click *Close*. +. Decrease the panel size, then drag and drop it to the first row next to the `Median of bytes` panel. There +should be four panels in a row. -. From the *Chart Type* dropdown, select *Pie*. -+ -[role="screenshot"] -image::images/lens_end_to_end_6_2.png[Files size distribution] - -. Click *Save and return*. +. You do not need a panel title because the axis labels are self-explanatory. [discrete] [[treemap]] -=== View the top sources of website traffic - -To determine how users find out about your website and where your users are located, create a treemap that displays the percentage of users that -enter your website from specific social media websites, and the top countries where users are located. +=== Create a multi-level chart -. Open *Lens*. +*Lens* lets you use multiple functions in the data table and proportion charts. For example, +to create a chart that breaks down the traffic sources and user geography, use *Filters* and +*Top values*. -. From the *Chart Type* dropdown, select *Treemap*. +. Click *Bar vertical stacked*, then select *Treemap*. . From the *Available fields* list, drag and drop *Records* to the *Size by* field in the editor. @@ -377,21 +335,15 @@ enter your website from specific social media websites, and the top countries wh .. Click *Add a filter*, enter the following, then press Return: -* *KQL* — `NOT referer : *twitter* OR NOT referer: *facebook.com*` +* *KQL* — `NOT referer : *twitter.com* OR NOT referer: *facebook.com*` * *Label* — `Other` .. Click *Close*. -[discrete] -[[add-the-countries]] -==== Add the geographic data - -To determine the top countries where users are located, add the geographic data. +Add the next break down by geography: -Compare the top sources of website traffic data to the top three countries. - -. From the *Available fields* list, drag and drop *geo.src* to the visualization builder. +. From the *Available fields* list, drag and drop *geo.src* to the main workspace. . To change the *Group by* order, click and drag *Top values of geo.src* so that it appears first in the editor. + @@ -409,6 +361,12 @@ image::images/lens_end_to_end_7_3.png[Group other values as Other] . Click *Save and return*. +. Arrange the panel so that it is in the same row as the table. + +.. Click the gear icon and choose *Edit panel title*. + +.. Enter "Page views by location and referer" as the panel title, then click *Save*. + [discrete] === Save the dashboard @@ -417,3 +375,5 @@ Now that you have a complete overview of your web server data, save the dashboar . In the toolbar, click *Save*. . On the *Save dashboard* window, enter `Web server data`, then click *Save*. + +. If this was not the first time you saved the dashboard, click *Switch to view mode* diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 6e3a7f697073d7c..ea413747a2aadf7 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -6,7 +6,7 @@ **_Gain insight to your data._** *Discover* enables you to quickly search and filter your data, get information -about structure of the fields, and visualize your data with *Lens* and *Maps*. +about the structure of the fields, and visualize your data with *Lens* and *Maps*. You can customize and save your searches and place them on a dashboard. ++++ @@ -110,6 +110,43 @@ image:images/document-table.png[Document table with fields for manufacturer, geo . To rearrange the table columns, hover the mouse over a column header, and then use the move and sort controls. +[float] +[[add-field-in-discover]] +=== Add a field + +What happens if you forgot to define an important value as a separate field? Or, what if you +want to combine two fields and treat them as one? +You can add a field to your index pattern from inside of **Discover**, +and then use that field for analysis and visualizations, +the same way you do with other fields. + +. Click the ellipsis icon (...), and then click *Add field to index pattern*. ++ +[role="screenshot"] +image:images/add-field-to-pattern.png[Dropdown menu located next to index pattern field with item for adding a field to an index pattern, width=50%] + +. In the *Create field* form, enter `hello` for the name. + +. Turn on *Set value*. + +. Use the Painless scripting language to define the field: ++ +```ts +emit("Hello World!"); +``` + +. Click *Save*. + +. In the fields list, search for the *hello* field, and then click it. ++ +You'll see the top values for the field. The pop-up also includes actions for filtering, +editing, and deleting the field. ++ +[role="screenshot"] +image:images/hello-field.png[Top values for the hello field, width=50%] + +For more information on adding fields and Painless scripting language examples, refer to <>. + [float] [[search-in-discover]] @@ -186,7 +223,8 @@ You can bookmark this document and share the link. === Save your search for later use Save your search so you can repeat it later, generate a CSV report, or use it in visualizations, dashboards, and Canvas workpads. -Saving a search saves the query and the filters. +Saving a search saves the query text, filters, +and current view of *Discover*—the columns selected in the document table, the sort order, and the index pattern. . In the toolbar, click **Save**. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 83e18734f65d433..397ab1717183b82 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -131,8 +131,8 @@ Kerberos, PKI, OIDC, and SAML. [cols="50, 50"] |=== -a| <> -|Create and manage the index patterns that retrieve your data from {es}. +a| <> +|Manage the data fields in the index patterns that retrieve your data from {es}. | <> | Copy, edit, delete, import, and export your saved objects. @@ -186,10 +186,10 @@ include::{kib-repo-dir}/management/managing-beats.asciidoc[] include::{kib-repo-dir}/management/action-types.asciidoc[] -include::{kib-repo-dir}/management/managing-fields.asciidoc[] - include::{kib-repo-dir}/management/managing-licenses.asciidoc[] +include::{kib-repo-dir}/management/manage-index-patterns.asciidoc[] + include::{kib-repo-dir}/management/numeral.asciidoc[] include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index c43e9210dd7c800..4305b39653f8dcd 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -16,6 +16,7 @@ Having trouble? Here are solutions to common problems you might encounter while * <> * <> * <> +* <> [float] [[reporting-diagnostics]] @@ -163,3 +164,12 @@ In this case, try increasing the memory for the {kib} instance to 2GB. === ARM systems Chromium is not compatible with ARM RHEL/CentOS. + +[float] +[[reporting-troubleshooting-maps-ems]] +=== Unable to connect to Elastic Maps Service + +https://www.elastic.co/elastic-maps-service[{ems} ({ems-init})] is a service that hosts +tile layers and vector shapes of administrative boundaries. +If a report contains a map with a missing basemap layer or administrative boundary, the {kib} server does not have access to {ems-init}. +See <> for information on how to connect your {kib} server to {ems-init}. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 805ae924a599e56..54142a6fe39e341 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -7,6 +7,7 @@ {kib} supports the following authentication mechanisms: +- <> - <> - <> - <> @@ -16,7 +17,12 @@ - <> - <> -Enable multiple authentication mechanisms at the same time specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start. +For an introduction to {kib}'s security features, including the login process, refer to <>. + +[[multiple-authentication-providers]] +==== Multiple authentication providers + +Enable multiple authentication mechanisms at the same time by specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start. When two or more providers are configured, you can choose the provider you want to use on the Login Selector UI. The order the providers appear is determined by the `order` setting. The appearance of the specific provider entry can be customized with the `description`, `hint`, and `icon` settings. @@ -24,7 +30,7 @@ TIP: To provide login instructions to users, use the `xpack.security.loginHelp` If you don't want a specific provider to show up at the Login Selector UI (e.g. to only support third-party initiated login) you can hide it with `showInSelector` setting set to `false`. However, in this case, the provider is presented in the provider chain and may be consulted during authentication based on its `order`. To disable the provider, use the `enabled` setting. -TIP: The Login Selector UI can also be disabled or enabled with `xpack.security.authc.selector.enabled` setting. +TIP: The Login Selector UI can also be disabled or enabled with `xpack.security.authc.selector.enabled` setting. Here is how your `kibana.yml` and Login Selector UI can look like if you deal with multiple authentication providers: @@ -292,9 +298,9 @@ xpack.security.authc.providers: order: 1 ----------------------------------------------- -IMPORTANT: {kib} uses SPNEGO, which wraps the Kerberos protocol for use with HTTP, extending it to web applications. +IMPORTANT: {kib} uses SPNEGO, which wraps the Kerberos protocol for use with HTTP, extending it to web applications. At the end of the Kerberos handshake, {kib} forwards the service ticket to {es}, then {es} unpacks the service ticket and responds with an access and refresh token, which are used for subsequent authentication. -On every {es} node that {kib} connects to, the keytab file should always contain the HTTP service principal for the {kib} host. +On every {es} node that {kib} connects to, the keytab file should always contain the HTTP service principal for the {kib} host. The HTTP service principal name must have the `HTTP/kibana.domain.local@KIBANA.DOMAIN.LOCAL` format. @@ -386,7 +392,7 @@ xpack.security.authc.providers: [[anonymous-access-and-embedding]] ===== Anonymous access and embedding -One of the most popular use cases for anonymous access is when you embed {kib} into other applications and don't want to force your users to log in to view it. +One of the most popular use cases for anonymous access is when you embed {kib} into other applications and don't want to force your users to log in to view it. If you configured {kib} to use anonymous access as the sole authentication mechanism, you don't need to do anything special while embedding {kib}. If you have multiple authentication providers enabled, and you want to automatically log in anonymous users when embedding dashboards and visualizations: diff --git a/examples/screenshot_mode_example/.i18nrc.json b/examples/screenshot_mode_example/.i18nrc.json new file mode 100644 index 000000000000000..cce0f6b34fea299 --- /dev/null +++ b/examples/screenshot_mode_example/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "screenshotModeExample", + "paths": { + "screenshotModeExample": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/examples/screenshot_mode_example/README.md b/examples/screenshot_mode_example/README.md new file mode 100755 index 000000000000000..ebae7480ca5fe0e --- /dev/null +++ b/examples/screenshot_mode_example/README.md @@ -0,0 +1,9 @@ +# screenshotModeExample + +A Kibana plugin + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/examples/screenshot_mode_example/common/index.ts b/examples/screenshot_mode_example/common/index.ts new file mode 100644 index 000000000000000..c6b22bdb077852e --- /dev/null +++ b/examples/screenshot_mode_example/common/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const PLUGIN_NAME = 'Screenshot mode example app'; + +export const BASE_API_ROUTE = '/api/screenshot_mode_example'; diff --git a/examples/screenshot_mode_example/kibana.json b/examples/screenshot_mode_example/kibana.json new file mode 100644 index 000000000000000..4cb8c1a1393fbdf --- /dev/null +++ b/examples/screenshot_mode_example/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "screenshotModeExample", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["navigation", "screenshotMode", "usageCollection"], + "optionalPlugins": [] +} diff --git a/examples/screenshot_mode_example/public/application.tsx b/examples/screenshot_mode_example/public/application.tsx new file mode 100644 index 000000000000000..670468c77bd5f51 --- /dev/null +++ b/examples/screenshot_mode_example/public/application.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '../../../src/core/public'; +import { AppPluginSetupDependencies, AppPluginStartDependencies } from './types'; +import { ScreenshotModeExampleApp } from './components/app'; + +export const renderApp = ( + { notifications, http }: CoreStart, + { screenshotMode }: AppPluginSetupDependencies, + { navigation }: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters +) => { + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/screenshot_mode_example/public/components/app.tsx b/examples/screenshot_mode_example/public/components/app.tsx new file mode 100644 index 000000000000000..c50eaf5b525683d --- /dev/null +++ b/examples/screenshot_mode_example/public/components/app.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect } from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { CoreStart } from '../../../../src/core/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { + ScreenshotModePluginSetup, + KBN_SCREENSHOT_MODE_HEADER, +} from '../../../../src/plugins/screenshot_mode/public'; + +import { PLUGIN_NAME, BASE_API_ROUTE } from '../../common'; + +interface ScreenshotModeExampleAppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; + screenshotMode: ScreenshotModePluginSetup; +} + +export const ScreenshotModeExampleApp = ({ + basename, + notifications, + http, + navigation, + screenshotMode, +}: ScreenshotModeExampleAppDeps) => { + const isScreenshotMode = screenshotMode.isScreenshotMode(); + + useEffect(() => { + // fire and forget + http.get(`${BASE_API_ROUTE}/check_is_screenshot`, { + headers: isScreenshotMode ? { [KBN_SCREENSHOT_MODE_HEADER]: 'true' } : undefined, + }); + notifications.toasts.addInfo({ + title: 'Welcome to the screenshot example app!', + text: isScreenshotMode + ? 'In screenshot mode we want this to remain visible' + : 'In normal mode this toast will disappear eventually', + toastLifeTimeMs: isScreenshotMode ? 360000 : 3000, + }); + }, [isScreenshotMode, notifications, http]); + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ {isScreenshotMode ? ( + + ) : ( + + )} +

+
+
+ + + {isScreenshotMode ? ( +

We detected screenshot mode. The chrome navbar should be hidden.

+ ) : ( +

+ This is how the app looks in normal mode. The chrome navbar should be + visible. +

+ )} +
+
+
+
+
+ +
+
+ ); +}; diff --git a/examples/screenshot_mode_example/public/index.scss b/examples/screenshot_mode_example/public/index.scss new file mode 100644 index 000000000000000..e69de29bb2d1d64 diff --git a/examples/screenshot_mode_example/public/index.ts b/examples/screenshot_mode_example/public/index.ts new file mode 100644 index 000000000000000..07768cbb1fdb717 --- /dev/null +++ b/examples/screenshot_mode_example/public/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './index.scss'; + +import { ScreenshotModeExamplePlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new ScreenshotModeExamplePlugin(); +} diff --git a/examples/screenshot_mode_example/public/plugin.ts b/examples/screenshot_mode_example/public/plugin.ts new file mode 100644 index 000000000000000..91bcc2410b5fc54 --- /dev/null +++ b/examples/screenshot_mode_example/public/plugin.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { AppPluginSetupDependencies, AppPluginStartDependencies } from './types'; +import { MetricsTracking } from './services'; +import { PLUGIN_NAME } from '../common'; + +export class ScreenshotModeExamplePlugin implements Plugin { + uiTracking = new MetricsTracking(); + + public setup(core: CoreSetup, depsSetup: AppPluginSetupDependencies): void { + const { screenshotMode, usageCollection } = depsSetup; + const isScreenshotMode = screenshotMode.isScreenshotMode(); + + this.uiTracking.setup({ + disableTracking: isScreenshotMode, // In screenshot mode there will be no user interactions to track + usageCollection, + }); + + // Register an application into the side navigation menu + core.application.register({ + id: 'screenshotModeExample', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + + // For screenshots we don't need to have the top bar visible + coreStart.chrome.setIsVisible(!isScreenshotMode); + + // Render the application + return renderApp(coreStart, depsSetup, depsStart as AppPluginStartDependencies, params); + }, + }); + } + + public start(core: CoreStart): void {} + + public stop() {} +} diff --git a/examples/screenshot_mode_example/public/services/index.ts b/examples/screenshot_mode_example/public/services/index.ts new file mode 100644 index 000000000000000..5725e52e65097bd --- /dev/null +++ b/examples/screenshot_mode_example/public/services/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { MetricsTracking } from './metrics_tracking'; diff --git a/examples/screenshot_mode_example/public/services/metrics_tracking.ts b/examples/screenshot_mode_example/public/services/metrics_tracking.ts new file mode 100644 index 000000000000000..e40b6bbf09e44fe --- /dev/null +++ b/examples/screenshot_mode_example/public/services/metrics_tracking.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UiCounterMetricType, METRIC_TYPE } from '@kbn/analytics'; +import { PLUGIN_NAME } from '../../common'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; + +export class MetricsTracking { + private trackingDisabled = false; + private usageCollection?: UsageCollectionSetup; + + private track(eventName: string, type: UiCounterMetricType) { + if (this.trackingDisabled) return; + + this.usageCollection?.reportUiCounter(PLUGIN_NAME, type, eventName); + } + + public setup({ + disableTracking, + usageCollection, + }: { + disableTracking?: boolean; + usageCollection: UsageCollectionSetup; + }) { + this.usageCollection = usageCollection; + if (disableTracking) this.trackingDisabled = true; + } + + public trackInit() { + this.track('init', METRIC_TYPE.LOADED); + } +} diff --git a/examples/screenshot_mode_example/public/types.ts b/examples/screenshot_mode_example/public/types.ts new file mode 100644 index 000000000000000..88812a4a507c91d --- /dev/null +++ b/examples/screenshot_mode_example/public/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; +import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/public'; +import { UsageCollectionSetup } from '../../../src/plugins/usage_collection/public'; + +export interface AppPluginSetupDependencies { + usageCollection: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; +} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; +} diff --git a/examples/screenshot_mode_example/server/index.ts b/examples/screenshot_mode_example/server/index.ts new file mode 100644 index 000000000000000..af23ea893a75536 --- /dev/null +++ b/examples/screenshot_mode_example/server/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext } from 'kibana/server'; +import { ScreenshotModeExamplePlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new ScreenshotModeExamplePlugin(ctx); diff --git a/examples/screenshot_mode_example/server/plugin.ts b/examples/screenshot_mode_example/server/plugin.ts new file mode 100644 index 000000000000000..5738f4a583a1a35 --- /dev/null +++ b/examples/screenshot_mode_example/server/plugin.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, PluginInitializerContext, CoreSetup, Logger } from 'kibana/server'; +import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/server'; +import { RouteDependencies } from './types'; +import { registerRoutes } from './routes'; + +export class ScreenshotModeExamplePlugin implements Plugin { + log: Logger; + constructor(ctx: PluginInitializerContext) { + this.log = ctx.logger.get(); + } + setup(core: CoreSetup, { screenshotMode }: { screenshotMode: ScreenshotModePluginSetup }): void { + const deps: RouteDependencies = { + screenshotMode, + router: core.http.createRouter(), + log: this.log, + }; + + registerRoutes(deps); + } + + start() {} + stop() {} +} diff --git a/examples/screenshot_mode_example/server/routes.ts b/examples/screenshot_mode_example/server/routes.ts new file mode 100644 index 000000000000000..adf4c2e2b6fc5b3 --- /dev/null +++ b/examples/screenshot_mode_example/server/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RouteDependencies } from './types'; +import { BASE_API_ROUTE } from '../common'; + +export const registerRoutes = ({ router, log, screenshotMode }: RouteDependencies) => { + router.get( + { path: `${BASE_API_ROUTE}/check_is_screenshot`, validate: false }, + async (ctx, req, res) => { + log.info(`Reading screenshot mode from a request: ${screenshotMode.isScreenshotMode(req)}`); + log.info(`Reading is screenshot mode from ctx: ${ctx.screenshotMode.isScreenshot}`); + return res.ok(); + } + ); +}; diff --git a/examples/screenshot_mode_example/server/types.ts b/examples/screenshot_mode_example/server/types.ts new file mode 100644 index 000000000000000..9d8d5888c3ab10c --- /dev/null +++ b/examples/screenshot_mode_example/server/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IRouter, Logger } from 'kibana/server'; +import { ScreenshotModeRequestHandlerContext } from '../../../src/plugins/screenshot_mode/server'; +import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/server'; + +export type ScreenshotModeExampleRouter = IRouter; + +export interface RouteDependencies { + screenshotMode: ScreenshotModePluginSetup; + router: ScreenshotModeExampleRouter; + log: Logger; +} diff --git a/examples/screenshot_mode_example/tsconfig.json b/examples/screenshot_mode_example/tsconfig.json new file mode 100644 index 000000000000000..dfb436e7377ac11 --- /dev/null +++ b/examples/screenshot_mode_example/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "common/**/*.ts", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [], + "references": [{ "path": "../../src/core/tsconfig.json" }] +} diff --git a/examples/search_examples/common/index.ts b/examples/search_examples/common/index.ts index cc47c0f57597397..c61de9d3c6dee2e 100644 --- a/examples/search_examples/common/index.ts +++ b/examples/search_examples/common/index.ts @@ -6,17 +6,7 @@ * Side Public License, v 1. */ -import { IEsSearchResponse, IEsSearchRequest } from '../../../src/plugins/data/common'; - export const PLUGIN_ID = 'searchExamples'; export const PLUGIN_NAME = 'Search Examples'; -export interface IMyStrategyRequest extends IEsSearchRequest { - get_cool: boolean; -} -export interface IMyStrategyResponse extends IEsSearchResponse { - cool: string; - executed_at: number; -} - export const SERVER_SEARCH_ROUTE_PATH = '/api/examples/search'; diff --git a/examples/search_examples/common/types.ts b/examples/search_examples/common/types.ts new file mode 100644 index 000000000000000..8bb38ea0b2d0d38 --- /dev/null +++ b/examples/search_examples/common/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + IEsSearchRequest, + IEsSearchResponse, + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../src/plugins/data/common'; + +export interface IMyStrategyRequest extends IEsSearchRequest { + get_cool: boolean; +} +export interface IMyStrategyResponse extends IEsSearchResponse { + cool: string; + executed_at: number; +} + +export type FibonacciRequest = IKibanaSearchRequest<{ n: number }>; + +export type FibonacciResponse = IKibanaSearchResponse<{ values: number[] }>; diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx index c9ede2ff2b45fb7..06f9426b4965cbd 100644 --- a/examples/search_examples/public/search/app.tsx +++ b/examples/search_examples/public/search/app.tsx @@ -26,27 +26,27 @@ import { EuiCode, EuiComboBox, EuiFormLabel, + EuiFieldNumber, + EuiProgress, EuiTabbedContent, + EuiTabbedContentTab, } from '@elastic/eui'; import { CoreStart } from '../../../../src/core/public'; import { mountReactNode } from '../../../../src/core/public/utils'; import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; -import { - PLUGIN_ID, - PLUGIN_NAME, - IMyStrategyResponse, - SERVER_SEARCH_ROUTE_PATH, -} from '../../common'; +import { PLUGIN_ID, PLUGIN_NAME, SERVER_SEARCH_ROUTE_PATH } from '../../common'; import { DataPublicPluginStart, + IKibanaSearchResponse, IndexPattern, IndexPatternField, isCompleteResponse, isErrorResponse, } from '../../../../src/plugins/data/public'; +import { IMyStrategyResponse } from '../../common/types'; interface SearchExamplesAppDeps { notifications: CoreStart['notifications']; @@ -88,7 +88,10 @@ export const SearchExamplesApp = ({ }: SearchExamplesAppDeps) => { const { IndexPatternSelect } = data.ui; const [getCool, setGetCool] = useState(false); + const [fibonacciN, setFibonacciN] = useState(10); const [timeTook, setTimeTook] = useState(); + const [total, setTotal] = useState(100); + const [loaded, setLoaded] = useState(0); const [indexPattern, setIndexPattern] = useState(); const [fields, setFields] = useState(); const [selectedFields, setSelectedFields] = useState([]); @@ -99,7 +102,15 @@ export const SearchExamplesApp = ({ IndexPatternField | null | undefined >(); const [request, setRequest] = useState>({}); - const [response, setResponse] = useState>({}); + const [rawResponse, setRawResponse] = useState>({}); + const [selectedTab, setSelectedTab] = useState(0); + + function setResponse(response: IKibanaSearchResponse) { + setRawResponse(response.rawResponse); + setLoaded(response.loaded!); + setTotal(response.total!); + setTimeTook(response.rawResponse.took); + } // Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted. useEffect(() => { @@ -152,8 +163,7 @@ export const SearchExamplesApp = ({ .subscribe({ next: (res) => { if (isCompleteResponse(res)) { - setResponse(res.rawResponse); - setTimeTook(res.rawResponse.took); + setResponse(res); const avgResult: number | undefined = res.rawResponse.aggregations ? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response res.rawResponse.aggregations[1].value @@ -234,7 +244,7 @@ export const SearchExamplesApp = ({ setRequest(searchSource.getSearchRequestBody()); const { rawResponse: res } = await searchSource.fetch$().toPromise(); - setResponse(res); + setRawResponse(res); const message = Searched {res.hits.total} documents.; notifications.toasts.addSuccess( @@ -247,7 +257,7 @@ export const SearchExamplesApp = ({ } ); } catch (e) { - setResponse(e.body); + setRawResponse(e.body); notifications.toasts.addWarning(`An error has occurred: ${e.message}`); } }; @@ -260,6 +270,41 @@ export const SearchExamplesApp = ({ doAsyncSearch('myStrategy'); }; + const onPartialResultsClickHandler = () => { + setSelectedTab(1); + const req = { + params: { + n: fibonacciN, + }, + }; + + // Submit the search request using the `data.search` service. + setRequest(req.params); + const searchSubscription$ = data.search + .search(req, { + strategy: 'fibonacciStrategy', + }) + .subscribe({ + next: (res) => { + setResponse(res); + if (isCompleteResponse(res)) { + notifications.toasts.addSuccess({ + title: 'Query result', + text: 'Query finished', + }); + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(res)) { + // TODO: Make response error status clearer + notifications.toasts.addWarning('An error has occurred'); + searchSubscription$.unsubscribe(); + } + }, + error: () => { + notifications.toasts.addDanger('Failed to run search'); + }, + }); + }; + const onClientSideSessionCacheClickHandler = () => { doAsyncSearch('myStrategy', data.search.session.getSessionId()); }; @@ -284,7 +329,7 @@ export const SearchExamplesApp = ({ doSearchSourceSearch(withOtherBucket); }; - const reqTabs = [ + const reqTabs: EuiTabbedContentTab[] = [ { id: 'request', name: Request, @@ -318,6 +363,7 @@ export const SearchExamplesApp = ({ values={{ time: timeTook ?? 'Unknown' }} /> + - {JSON.stringify(response, null, 2)} + {JSON.stringify(rawResponse, null, 2)} ), @@ -484,6 +530,37 @@ export const SearchExamplesApp = ({ + +

Handling partial results

+
+ + The observable returned from data.search provides partial results + when the response is not yet complete. These can be handled to update a chart or + simply a progress bar: + + + <EuiProgress value={response.loaded} max={response.total} + /> + + Below is an example showing a custom search strategy that emits partial Fibonacci + sequences up to the length provided, updates the response with each partial result, + and updates a progress bar (see the Response tab). + setFibonacciN(parseInt(event.target.value, 10))} + /> + + Request Fibonacci sequence + + +

Writing a custom search strategy

@@ -567,8 +644,13 @@ export const SearchExamplesApp = ({ + - + setSelectedTab(reqTabs.indexOf(tab))} + /> diff --git a/examples/search_examples/server/fibonacci_strategy.ts b/examples/search_examples/server/fibonacci_strategy.ts new file mode 100644 index 000000000000000..a37438aba7055c4 --- /dev/null +++ b/examples/search_examples/server/fibonacci_strategy.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import uuid from 'uuid'; +import { ISearchStrategy } from '../../../src/plugins/data/server'; +import { FibonacciRequest, FibonacciResponse } from '../common/types'; + +export const fibonacciStrategyProvider = (): ISearchStrategy< + FibonacciRequest, + FibonacciResponse +> => { + const responseMap = new Map(); + return ({ + search: (request: FibonacciRequest) => { + const id = request.id ?? uuid(); + const [sequence, total, started] = responseMap.get(id) ?? [ + [], + request.params?.n ?? 0, + Date.now(), + ]; + if (sequence.length < 2) { + if (total > 0) sequence.push(sequence.length); + } else { + const [a, b] = sequence.slice(-2); + sequence.push(a + b); + } + const loaded = sequence.length; + responseMap.set(id, [sequence, total, started]); + if (loaded >= total) { + responseMap.delete(id); + } + + const isRunning = loaded < total; + const isPartial = isRunning; + const took = Date.now() - started; + const values = sequence.slice(0, loaded); + + // Usually we'd do something like "of()" but for some reason it breaks in tests with the error + // "You provided an invalid object where a stream was expected." which is why we have to cast + // down below as well + return [{ id, loaded, total, isRunning, isPartial, rawResponse: { took, values } }]; + }, + cancel: async (id: string) => { + responseMap.delete(id); + }, + } as unknown) as ISearchStrategy; +}; diff --git a/examples/search_examples/server/my_strategy.ts b/examples/search_examples/server/my_strategy.ts index 0a647889600912e..db8cd903f23d69f 100644 --- a/examples/search_examples/server/my_strategy.ts +++ b/examples/search_examples/server/my_strategy.ts @@ -8,7 +8,7 @@ import { map } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../src/plugins/data/server'; -import { IMyStrategyResponse, IMyStrategyRequest } from '../common'; +import { IMyStrategyRequest, IMyStrategyResponse } from '../common/types'; export const mySearchStrategyProvider = ( data: PluginStart diff --git a/examples/search_examples/server/plugin.ts b/examples/search_examples/server/plugin.ts index 84f082d890bb0f6..984d3201220eb2e 100644 --- a/examples/search_examples/server/plugin.ts +++ b/examples/search_examples/server/plugin.ts @@ -24,6 +24,7 @@ import { } from './types'; import { mySearchStrategyProvider } from './my_strategy'; import { registerRoutes } from './routes'; +import { fibonacciStrategyProvider } from './fibonacci_strategy'; export class SearchExamplesPlugin implements @@ -48,7 +49,9 @@ export class SearchExamplesPlugin core.getStartServices().then(([_, depsStart]) => { const myStrategy = mySearchStrategyProvider(depsStart.data); + const fibonacciStrategy = fibonacciStrategyProvider(); deps.data.search.registerSearchStrategy('myStrategy', myStrategy); + deps.data.search.registerSearchStrategy('fibonacciStrategy', fibonacciStrategy); registerRoutes(router); }); diff --git a/package.json b/package.json index 211cbed02d4c74a..7966f33f262492f 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "**/load-grunt-config/lodash": "^4.17.21", "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", + "**/pdfkit/crypto-js": "4.0.0", "**/prismjs": "1.23.0", "**/react-syntax-highlighter": "^15.3.1", "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", @@ -92,7 +93,7 @@ "**/underscore": "^1.13.1" }, "engines": { - "node": "14.16.1", + "node": "14.17.0", "yarn": "^1.21.1" }, "dependencies": { @@ -107,12 +108,13 @@ "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", - "@elastic/numeral": "^2.5.0", + "@elastic/numeral": "^2.5.1", "@elastic/react-search-ui": "^1.5.1", "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "link:bazel-bin/packages/elastic-safer-lodash-set/npm_module", "@elastic/search-ui-app-search-connector": "^1.5.0", "@elastic/ui-ace": "0.2.3", + "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.1", "@hapi/cookie": "^11.0.2", "@hapi/good-squeeze": "6.0.0", @@ -129,16 +131,20 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", - "@kbn/i18n": "link:packages/kbn-i18n", + "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", - "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", + "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", + "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", + "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", + "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", - "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", + "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", + "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools/npm_module", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath/npm_module", @@ -167,7 +173,6 @@ "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", - "accept": "3.0.2", "ajv": "^6.12.4", "angular": "^1.8.0", "angular-aria": "^1.8.0", @@ -264,7 +269,8 @@ "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", - "kea": "^2.3.0", + "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", + "kea": "^2.4.2", "leaflet": "1.5.1", "leaflet-draw": "0.4.14", "leaflet-responsive-popup": "0.6.4", @@ -298,7 +304,6 @@ "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", "opn": "^5.5.0", - "oppsy": "^2.0.0", "p-limit": "^3.0.1", "p-map": "^4.0.0", "p-retry": "^4.2.0", @@ -314,7 +319,7 @@ "proxy-from-env": "1.0.0", "proxyquire": "1.8.0", "puid": "1.0.7", - "puppeteer": "npm:@elastic/puppeteer@5.4.1-patch.1", + "puppeteer": "^8.0.0", "query-string": "^6.13.2", "raw-loader": "^3.1.0", "rbush": "^3.0.1", @@ -431,7 +436,7 @@ "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", "@bazel/ibazel": "^0.15.10", - "@bazel/typescript": "^3.4.2", + "@bazel/typescript": "^3.5.0", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/apm-rum": "^5.6.1", @@ -446,18 +451,18 @@ "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset/npm_module", "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module", - "@kbn/docs-utils": "link:packages/kbn-docs-utils", + "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module", "@kbn/es": "link:bazel-bin/packages/kbn-es/npm_module", "@kbn/es-archiver": "link:packages/kbn-es-archiver", "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana/npm_module", "@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint/npm_module", "@kbn/expect": "link:bazel-bin/packages/kbn-expect/npm_module", "@kbn/optimizer": "link:packages/kbn-optimizer", - "@kbn/plugin-generator": "link:packages/kbn-plugin-generator", + "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator/npm_module", "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", "@kbn/storybook": "link:packages/kbn-storybook", - "@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools", + "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", "@loaders.gl/polyfills": "^2.3.5", @@ -484,7 +489,6 @@ "@testing-library/react": "^11.2.6", "@testing-library/react-hooks": "^5.1.1", "@testing-library/user-event": "^13.1.1", - "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", "@types/archiver": "^5.1.0", @@ -586,7 +590,6 @@ "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", - "@types/puppeteer": "^5.4.1", "@types/rbush": "^3.0.0", "@types/reach__router": "^1.2.6", "@types/react": "^16.9.36", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 8b483f54344e2b5..76250d8a1e86487 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -16,16 +16,25 @@ filegroup( "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", "//packages/kbn-dev-utils:build", + "//packages/kbn-docs-utils:build", "//packages/kbn-es:build", "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", + "//packages/kbn-i18n:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", + "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-constants:build", + "//packages/kbn-securitysolution-io-ts-types:build", + "//packages/kbn-securitysolution-io-ts-alerting-types:build", + "//packages/kbn-securitysolution-io-ts-list-types:build", "//packages/kbn-securitysolution-io-ts-utils:build", "//packages/kbn-securitysolution-utils:build", + "//packages/kbn-securitysolution-es-utils:build", + "//packages/kbn-server-http-tools:build", "//packages/kbn-std:build", + "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", "//packages/kbn-utility-types:build", "//packages/kbn-utils:build", diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index 0401e6a82e11a7f..dd491de55c075dc 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/server-http-tools": "link:../kbn-server-http-tools", "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts index f2c0a4391634321..47746967bbe5f1f 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts @@ -36,10 +36,9 @@ describe('applyDeprecations', () => { const addDeprecation = jest.fn(); const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; - const alteredConfig = { foo: 'bar' }; - const handlerA = jest.fn().mockReturnValue(alteredConfig); - const handlerB = jest.fn().mockImplementation((conf) => conf); + const handlerA = jest.fn().mockReturnValue({ unset: [{ path: 'deprecated' }] }); + const handlerB = jest.fn().mockReturnValue(undefined); applyDeprecations( initialConfig, @@ -47,8 +46,6 @@ describe('applyDeprecations', () => { createAddDeprecation ); - expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation); - expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation); expect(createAddDeprecation).toBeCalledTimes(2); expect(createAddDeprecation).toHaveBeenNthCalledWith(1, 'pathA'); expect(createAddDeprecation).toHaveBeenNthCalledWith(2, 'pathB'); @@ -60,8 +57,15 @@ describe('applyDeprecations', () => { const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; const alteredConfig = { foo: 'bar' }; - const handlerA = jest.fn().mockReturnValue(alteredConfig); - const handlerB = jest.fn().mockImplementation((conf) => conf); + const configs: Array<{ fn: string; config: Record }> = []; + const handlerA = jest.fn().mockImplementation((config) => { + // the first argument is mutated between calls, we store a copy of it + configs.push({ fn: 'handlerA', config: { ...config } }); + return { unset: [{ path: 'deprecated' }] }; + }); + const handlerB = jest.fn().mockImplementation((config) => { + configs.push({ fn: 'handlerB', config: { ...config } }); + }); applyDeprecations( initialConfig, @@ -69,8 +73,10 @@ describe('applyDeprecations', () => { createAddDeprecation ); - expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation); - expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation); + expect(configs).toEqual([ + { fn: 'handlerA', config: initialConfig }, + { fn: 'handlerB', config: alteredConfig }, + ]); }); it('returns the migrated config', () => { @@ -94,4 +100,40 @@ describe('applyDeprecations', () => { expect(initialConfig).toEqual({ foo: 'bar', deprecated: 'deprecated' }); expect(migrated).toEqual({ foo: 'bar' }); }); + + it('ignores a command for unknown path', () => { + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + + const handler = jest.fn().mockImplementation((config) => { + return { unset: [{ path: 'unknown' }] }; + }); + + const migrated = applyDeprecations( + initialConfig, + [wrapHandler(handler, 'pathA')], + createAddDeprecation + ); + + expect(migrated).toEqual(initialConfig); + }); + + it('ignores an unknown command', () => { + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + + const handler = jest.fn().mockImplementation((config) => { + return { rewrite: [{ path: 'foo' }] }; + }); + + const migrated = applyDeprecations( + initialConfig, + [wrapHandler(handler, 'pathA')], + createAddDeprecation + ); + + expect(migrated).toEqual(initialConfig); + }); }); diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.ts b/packages/kbn-config/src/deprecation/apply_deprecations.ts index 6aced541dc30d45..092a5ced28371d7 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, unset } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { ConfigDeprecationWithContext, AddConfigDeprecation } from './types'; const noopAddDeprecationFactory: () => AddConfigDeprecation = () => () => undefined; @@ -22,9 +23,21 @@ export const applyDeprecations = ( deprecations: ConfigDeprecationWithContext[], createAddDeprecation: (pluginId: string) => AddConfigDeprecation = noopAddDeprecationFactory ) => { - let processed = cloneDeep(config); + const result = cloneDeep(config); deprecations.forEach(({ deprecation, path }) => { - processed = deprecation(processed, path, createAddDeprecation(path)); + const commands = deprecation(result, path, createAddDeprecation(path)); + if (commands) { + if (commands.set) { + commands.set.forEach(function ({ path: commandPath, value }) { + set(result, commandPath, value); + }); + } + if (commands.unset) { + commands.unset.forEach(function ({ path: commandPath }) { + unset(result, commandPath); + }); + } + } }); - return processed; + return result; }; diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index 11a49ed79d17012..563d4017f5ed950 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -29,15 +29,15 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); - expect(processed).toEqual({ - myplugin: { - renamed: 'toberenamed', - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, + const commands = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toEqual({ + set: [ + { + path: 'myplugin.renamed', + value: 'toberenamed', + }, + ], + unset: [{ path: 'myplugin.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ @@ -64,16 +64,8 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', addDeprecation); - expect(processed).toEqual({ - myplugin: { - new: 'new', - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, - }); + const commands = rename('deprecated', 'new')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toBeUndefined(); expect(addDeprecation).toHaveBeenCalledTimes(0); }); it('handles nested keys', () => { @@ -88,22 +80,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('oldsection.deprecated', 'newsection.renamed')( + const commands = rename('oldsection.deprecated', 'newsection.renamed')( rawConfig, 'myplugin', addDeprecation ); - expect(processed).toEqual({ - myplugin: { - oldsection: {}, - newsection: { - renamed: 'toberenamed', + expect(commands).toEqual({ + set: [ + { + path: 'myplugin.newsection.renamed', + value: 'toberenamed', }, - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, + ], + unset: [{ path: 'myplugin.oldsection.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ @@ -127,11 +116,9 @@ describe('DeprecationFactory', () => { renamed: 'renamed', }, }; - const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); - expect(processed).toEqual({ - myplugin: { - renamed: 'renamed', - }, + const commands = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toEqual({ + unset: [{ path: 'myplugin.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ @@ -162,19 +149,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( + const commands = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( rawConfig, 'does-not-matter', addDeprecation ); - expect(processed).toEqual({ - myplugin: { - renamed: 'toberenamed', - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, + expect(commands).toEqual({ + set: [ + { + path: 'myplugin.renamed', + value: 'toberenamed', + }, + ], + unset: [{ path: 'myplugin.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ @@ -202,19 +189,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')( + const commands = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')( rawConfig, 'does-not-matter', addDeprecation ); - expect(processed).toEqual({ - oldplugin: { - valid: 'valid', - }, - newplugin: { - renamed: 'toberenamed', - property: 'value', - }, + expect(commands).toEqual({ + set: [ + { + path: 'newplugin.renamed', + value: 'toberenamed', + }, + ], + unset: [{ path: 'oldplugin.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ @@ -242,20 +229,12 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = renameFromRoot('myplugin.deprecated', 'myplugin.new')( + const commands = renameFromRoot('myplugin.deprecated', 'myplugin.new')( rawConfig, 'does-not-matter', addDeprecation ); - expect(processed).toEqual({ - myplugin: { - new: 'new', - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, - }); + expect(commands).toBeUndefined(); expect(addDeprecation).toBeCalledTimes(0); }); @@ -266,15 +245,13 @@ describe('DeprecationFactory', () => { renamed: 'renamed', }, }; - const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( + const commands = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( rawConfig, 'does-not-matter', addDeprecation ); - expect(processed).toEqual({ - myplugin: { - renamed: 'renamed', - }, + expect(commands).toEqual({ + unset: [{ path: 'myplugin.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` @@ -306,14 +283,9 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); - expect(processed).toEqual({ - myplugin: { - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, + const commands = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toEqual({ + unset: [{ path: 'myplugin.deprecated' }], }); expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ @@ -343,17 +315,10 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('section.deprecated')(rawConfig, 'myplugin', addDeprecation); - expect(processed).toEqual({ - myplugin: { - valid: 'valid', - section: {}, - }, - someOtherPlugin: { - property: 'value', - }, + const commands = unused('section.deprecated')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toEqual({ + unset: [{ path: 'myplugin.section.deprecated' }], }); - expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -379,15 +344,8 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); - expect(processed).toEqual({ - myplugin: { - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, - }); + const commands = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toBeUndefined(); expect(addDeprecation).toBeCalledTimes(0); }); }); @@ -403,20 +361,14 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unusedFromRoot('myplugin.deprecated')( + const commands = unusedFromRoot('myplugin.deprecated')( rawConfig, 'does-not-matter', addDeprecation ); - expect(processed).toEqual({ - myplugin: { - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, + expect(commands).toEqual({ + unset: [{ path: 'myplugin.deprecated' }], }); - expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -442,19 +394,12 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unusedFromRoot('myplugin.deprecated')( + const commands = unusedFromRoot('myplugin.deprecated')( rawConfig, 'does-not-matter', addDeprecation ); - expect(processed).toEqual({ - myplugin: { - valid: 'valid', - }, - someOtherPlugin: { - property: 'value', - }, - }); + expect(commands).toBeUndefined(); expect(addDeprecation).toBeCalledTimes(0); }); }); diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 140846d86ae0b4b..76bcc1958d0de42 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -7,13 +7,12 @@ */ import { get } from 'lodash'; -import { set } from '@elastic/safer-lodash-set'; -import { unset } from '@kbn/std'; import { ConfigDeprecation, AddConfigDeprecation, ConfigDeprecationFactory, DeprecatedConfigDetails, + ConfigDeprecationCommand, } from './types'; const _rename = ( @@ -23,20 +22,16 @@ const _rename = ( oldKey: string, newKey: string, details?: Partial -) => { +): void | ConfigDeprecationCommand => { const fullOldPath = getPath(rootPath, oldKey); const oldValue = get(config, fullOldPath); if (oldValue === undefined) { - return config; + return; } - unset(config, fullOldPath); - const fullNewPath = getPath(rootPath, newKey); const newValue = get(config, fullNewPath); if (newValue === undefined) { - set(config, fullNewPath, oldValue); - addDeprecation({ message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`, correctiveActions: { @@ -46,6 +41,10 @@ const _rename = ( }, ...details, }); + return { + set: [{ path: fullNewPath, value: oldValue }], + unset: [{ path: fullOldPath }], + }; } else { addDeprecation({ message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"`, @@ -59,7 +58,9 @@ const _rename = ( }); } - return config; + return { + unset: [{ path: fullOldPath }], + }; }; const _unused = ( @@ -68,12 +69,11 @@ const _unused = ( addDeprecation: AddConfigDeprecation, unusedKey: string, details?: Partial -) => { +): void | ConfigDeprecationCommand => { const fullPath = getPath(rootPath, unusedKey); if (get(config, fullPath) === undefined) { - return config; + return; } - unset(config, fullPath); addDeprecation({ message: `${fullPath} is deprecated and is no longer used`, correctiveActions: { @@ -83,7 +83,9 @@ const _unused = ( }, ...details, }); - return config; + return { + unset: [{ path: fullPath }], + }; }; const rename = ( diff --git a/packages/kbn-config/src/deprecation/index.ts b/packages/kbn-config/src/deprecation/index.ts index 3286acca9e584a5..48576e6d830befa 100644 --- a/packages/kbn-config/src/deprecation/index.ts +++ b/packages/kbn-config/src/deprecation/index.ts @@ -8,6 +8,7 @@ export type { ConfigDeprecation, + ConfigDeprecationCommand, ConfigDeprecationWithContext, ConfigDeprecationFactory, AddConfigDeprecation, diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 3b1d004d7ec761b..6944f45c1e1d227 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import type { RecursiveReadonly } from '@kbn/utility-types'; /** * Config deprecation hook used when invoking a {@link ConfigDeprecation} * @@ -41,14 +41,29 @@ export interface DeprecatedConfigDetails { * @remarks * This should only be manually implemented if {@link ConfigDeprecationFactory} does not provide the proper helpers for a specific * deprecation need. + * @param config must not be mutated, return {@link ConfigDeprecationCommand} to change config shape. * - * @public + * @example + * ```typescript + * const provider: ConfigDeprecation = (config, path) => ({ unset: [{ key: 'path.to.key' }] }) + * ``` + * @internal */ export type ConfigDeprecation = ( - config: Record, + config: RecursiveReadonly>, fromPath: string, addDeprecation: AddConfigDeprecation -) => Record; +) => void | ConfigDeprecationCommand; + +/** + * Outcome of deprecation operation. Allows mutating config values in a declarative way. + * + * @public + */ +export interface ConfigDeprecationCommand { + set?: Array<{ path: string; value: any }>; + unset?: Array<{ path: string }>; +} /** * A provider that should returns a list of {@link ConfigDeprecation}. @@ -60,7 +75,7 @@ export type ConfigDeprecation = ( * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ * rename('oldKey', 'newKey'), * unused('deprecatedKey'), - * myCustomDeprecation, + * (config, path) => ({ unset: [{ key: 'path.to.key' }] }) * ] * ``` * diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index a9ea8265a37682f..cf875d3daa4a21c 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -12,6 +12,7 @@ export type { ConfigDeprecationProvider, ConfigDeprecationWithContext, ConfigDeprecation, + ConfigDeprecationCommand, } from './deprecation'; export { applyDeprecations, configDeprecationFactory } from './deprecation'; diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 8f55f0e0f06a794..14e292c056db695 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -2,7 +2,7 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -PKG_BASE_NAME = "kbn-cypto" +PKG_BASE_NAME = "kbn-crypto" PKG_REQUIRE_NAME = "@kbn/crypto" SOURCE_FILES = glob( diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel new file mode 100644 index 000000000000000..e72d83851f5d2ee --- /dev/null +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -0,0 +1,88 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-docs-utils" +PKG_REQUIRE_NAME = "@kbn/docs-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/snapshots/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "//packages/kbn-config", + "//packages/kbn-dev-utils", + "//packages/kbn-utils", + "@npm//dedent", + "@npm//ts-morph", +] + +TYPES_DEPS = [ + "@npm//@types/dedent", + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 27d38d2d8ed4fc4..b2a52b2d1f78e7f 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -7,9 +7,5 @@ "types": "target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "kbn:bootstrap": "../../node_modules/.bin/tsc", - "kbn:watch": "../../node_modules/.bin/tsc --watch" } } \ No newline at end of file diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 6f4a6fa2af8a550..9868c8b3d2bb4ac 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "target": "ES2019", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-docs-utils/src", "types": [ diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index fbef255cd9ee5e0..51d4f28d20f2e0b 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -165,6 +165,7 @@ export async function createDefaultSpace({ { index, id: 'space:default', + refresh: 'wait_for', body: { type: 'space', updated_at: new Date().toISOString(), diff --git a/packages/kbn-es/README.md b/packages/kbn-es/README.md index 4d4c2aa94db07fd..80850c9e6a09cc0 100644 --- a/packages/kbn-es/README.md +++ b/packages/kbn-es/README.md @@ -7,6 +7,8 @@ If running elasticsearch from source, elasticsearch needs to be cloned to a sibl To run, go to the Kibana root and run `node scripts/es --help` to get the latest command line options. +The script attempts to preserve the existing interfaces used by Elasticsearch CLI. This includes passing through options with the `-E` argument and the `ES_JAVA_OPTS` environment variable for Java options. + ### Examples Run a snapshot install with a trial license diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index c55e5d3513c44e4..ad9ecb059031cd4 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -236,6 +236,7 @@ exports.Cluster = class Cluster { * @param {String} installPath * @param {Object} options * @property {string|Array} options.esArgs + * @property {string} options.esJavaOpts * @return {undefined} */ _exec(installPath, options = {}) { @@ -268,14 +269,17 @@ exports.Cluster = class Cluster { this._log.debug('%s %s', ES_BIN, args.join(' ')); - options.esEnvVars = options.esEnvVars || {}; + let esJavaOpts = `${options.esJavaOpts || ''} ${process.env.ES_JAVA_OPTS || ''}`; // ES now automatically sets heap size to 50% of the machine's available memory // so we need to set it to a smaller size for local dev and CI // especially because we currently run many instances of ES on the same machine during CI - options.esEnvVars.ES_JAVA_OPTS = - (options.esEnvVars.ES_JAVA_OPTS ? `${options.esEnvVars.ES_JAVA_OPTS} ` : '') + - '-Xms1g -Xmx1g'; + // inital and max must be the same, so we only need to check the max + if (!esJavaOpts.includes('Xmx')) { + esJavaOpts += ' -Xms1g -Xmx1g'; + } + + this._log.debug('ES_JAVA_OPTS: %s', esJavaOpts.trim()); this._process = execa(ES_BIN, args, { cwd: installPath, @@ -283,7 +287,7 @@ exports.Cluster = class Cluster { ...(installPath ? { ES_TMPDIR: path.resolve(installPath, 'ES_TMPDIR') } : {}), ...process.env, JAVA_HOME: '', // By default, we want to always unset JAVA_HOME so that the bundled JDK will be used - ...(options.esEnvVars || {}), + ES_JAVA_OPTS: esJavaOpts.trim(), }, stdio: ['ignore', 'pipe', 'pipe'], }); diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index 6b4025840283f59..34220b08d212096 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -71,11 +71,17 @@ function mockEsBin({ exitCode, start }) { ); } +const initialEnv = { ...process.env }; + beforeEach(() => { jest.resetAllMocks(); extractConfigFiles.mockImplementation((config) => config); }); +afterEach(() => { + process.env = { ...initialEnv }; +}); + describe('#installSource()', () => { it('awaits installSource() promise and returns { installPath }', async () => { let resolveInstallSource; @@ -355,6 +361,25 @@ describe('#run()', () => { ] `); }); + + it('sets default Java heap', async () => { + mockEsBin({ start: true }); + + const cluster = new Cluster({ log }); + await cluster.run(); + + expect(execa.mock.calls[0][2].env.ES_JAVA_OPTS).toEqual('-Xms1g -Xmx1g'); + }); + + it('allows Java heap to be overwritten', async () => { + mockEsBin({ start: true }); + process.env.ES_JAVA_OPTS = '-Xms5g -Xmx5g'; + + const cluster = new Cluster({ log }); + await cluster.run(); + + expect(execa.mock.calls[0][2].env.ES_JAVA_OPTS).toEqual('-Xms5g -Xmx5g'); + }); }); describe('#stop()', () => { diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel new file mode 100644 index 000000000000000..02f5874a69a8389 --- /dev/null +++ b/packages/kbn-i18n/BUILD.bazel @@ -0,0 +1,132 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-i18n" +PKG_REQUIRE_NAME = "@kbn/i18n" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/core/locales.js", + "types/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/__snapshots__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "angular/package.json", + "react/package.json", + "package.json", + "GUIDELINE.md", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//del", + "@npm//getopts", + "@npm//intl-format-cache", + "@npm//intl-messageformat", + "@npm//intl-relativeformat", + "@npm//prop-types", + "@npm//react", + "@npm//react-intl", + "@npm//supports-color", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/angular", + "@npm//@types/intl-relativeformat", + "@npm//@types/jest", + "@npm//@types/prop-types", + "@npm//@types/react", + "@npm//@types/react-intl", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + incremental = True, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc", ":tsc_browser"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-i18n/angular/package.json b/packages/kbn-i18n/angular/package.json index 974058ec0ac9141..11c842a9fc49b2e 100644 --- a/packages/kbn-i18n/angular/package.json +++ b/packages/kbn-i18n/angular/package.json @@ -1,5 +1,5 @@ { - "browser": "../target/web/angular", - "main": "../target/node/angular", - "types": "../target/types/angular/index.d.ts" + "browser": "../target_web/angular", + "main": "../target_node/angular", + "types": "../target_types/angular/index.d.ts" } \ No newline at end of file diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 1f9d21f724ea891..d91b81a88e098da 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -1,14 +1,9 @@ { "name": "@kbn/i18n", - "browser": "./target/web/browser.js", - "main": "./target/node/index.js", - "types": "./target/types/index.d.ts", + "browser": "./target_web/browser.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --watch --source-maps" - } + "private": true } \ No newline at end of file diff --git a/packages/kbn-i18n/react/package.json b/packages/kbn-i18n/react/package.json index d4cf1a0a30f61c0..c29ddd45f084d8c 100644 --- a/packages/kbn-i18n/react/package.json +++ b/packages/kbn-i18n/react/package.json @@ -1,5 +1,5 @@ { - "browser": "../target/web/react", - "main": "../target/node/react", - "types": "../target/types/react/index.d.ts" + "browser": "../target_web/react", + "main": "../target_node/react", + "types": "../target_types/react/index.d.ts" } \ No newline at end of file diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js deleted file mode 100644 index 62ef2f59239d0a5..000000000000000 --- a/packages/kbn-i18n/scripts/build.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { resolve } = require('path'); - -const del = require('del'); -const supportsColor = require('supports-color'); -const { run, withProcRunner } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -run( - async ({ log, flags }) => { - await withProcRunner(log, async (proc) => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['web', 'node'].map((subTask) => - proc.run(padRight(10, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.ts,.js,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE - ? [] - : ['--source-maps', 'inline']), - ], - wait: true, - env: { - ...env, - BABEL_ENV: subTask, - }, - cwd, - }) - ), - - proc.run(padRight(10, 'tsc'), { - cmd: 'tsc', - args: [ - ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), - ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); - }); - }, - { - description: 'Simple build tool for @kbn/i18n package', - flags: { - boolean: ['watch', 'source-maps'], - help: ` - --watch Run in watch mode - --source-maps Include sourcemaps - `, - }, - } -); diff --git a/packages/kbn-i18n/tsconfig.browser.json b/packages/kbn-i18n/tsconfig.browser.json new file mode 100644 index 000000000000000..707e3294bf1e7b8 --- /dev/null +++ b/packages/kbn-i18n/tsconfig.browser.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target_web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-i18n/src", + "types": ["node"], + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "types/intl_format_cache.d.ts", + "types/intl_relativeformat.d.ts" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index 9d4cb8c9b0972b9..787e9b45123ace8 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, - "outDir": "./target/types", - "emitDeclarationOnly": true, + "allowJs": true, + "incremental": true, + "declarationDir": "./target_types", + "outDir": "./target_node", "declaration": true, "declarationMap": true, "sourceMap": true, diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 997fbb0eb8a4ff0..fc0936f4b5f53bf 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -8,8 +8,5 @@ "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --dev --watch" - }, - "dependencies": { - "@kbn/i18n": "link:../kbn-i18n" } } \ No newline at end of file diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts index 4d57d869b9008f3..4b1e34839030f48 100644 --- a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts @@ -149,7 +149,7 @@ export class LogRotator { if (this.usePolling && !this.shouldUsePolling) { this.log( ['warning', 'logging:rotate'], - 'Looks like your current environment support a faster algorithm then polling. You can try to disable `usePolling`' + 'Looks like your current environment support a faster algorithm than polling. You can try to disable `usePolling`' ); } diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index 75f1d74f1c9c965..e818351e7e4700d 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -9,8 +9,5 @@ "build": "node ./scripts/build.js", "kbn:bootstrap": "yarn build --dev", "build:antlr4ts": "../../node_modules/antlr4ts-cli/antlr4ts ./src/painless/antlr/painless_lexer.g4 ./src/painless/antlr/painless_parser.g4 && node ./scripts/fix_generated_antlr.js" - }, - "dependencies": { - "@kbn/i18n": "link:../kbn-i18n" } } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 63dd64f9202b3b6..2639f6fd273f7b2 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 228500 + lists: 200000 logstash: 53548 management: 46112 maps: 80000 @@ -61,7 +61,6 @@ pageLoadAssetSize: remoteClusters: 51327 reporting: 183418 rollup: 97204 - ruleRegistry: 100000 savedObjects: 108518 savedObjectsManagement: 101836 savedObjectsTagging: 59482 @@ -69,7 +68,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 95864 securityOss: 30806 - securitySolution: 187863 + securitySolution: 76000 share: 99061 snapshotRestore: 79032 spaces: 57868 @@ -111,3 +110,4 @@ pageLoadAssetSize: mapsEms: 26072 timelines: 28613 cases: 162385 + screenshotMode: 17856 diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel new file mode 100644 index 000000000000000..e22d41076db00f7 --- /dev/null +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -0,0 +1,106 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-plugin-generator" +PKG_REQUIRE_NAME = "@kbn/plugin-generator" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/integration_tests/**/*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +filegroup( + name = "template", + srcs = glob( + [ + "template/**/*", + ], + ), +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", + ":template", +] + +SRC_DEPS = [ + "//packages/kbn-utils", + "//packages/kbn-dev-utils", + "@npm//del", + "@npm//ejs", + "@npm//execa", + "@npm//globby", + "@npm//inquirer", + "@npm//minimatch", + "@npm//prettier", + "@npm//vinyl-fs", +] + +TYPES_DEPS = [ + "@npm//@types/ejs", + "@npm//@types/inquirer", + "@npm//@types/jest", + "@npm//@types/minimatch", + "@npm//@types/node", + "@npm//@types/prettier", + "@npm//@types/vinyl-fs", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-plugin-generator/package.json b/packages/kbn-plugin-generator/package.json index 583085430d915b6..298373afd2f249a 100644 --- a/packages/kbn-plugin-generator/package.json +++ b/packages/kbn-plugin-generator/package.json @@ -4,9 +4,5 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "target/index.js", - "types": "target/index.d.ts", - "scripts": { - "kbn:bootstrap": "node scripts/build", - "kbn:watch": "node scripts/build --watch" - } + "types": "target/index.d.ts" } \ No newline at end of file diff --git a/packages/kbn-plugin-generator/scripts/build.js b/packages/kbn-plugin-generator/scripts/build.js deleted file mode 100644 index e17f564bc482c00..000000000000000 --- a/packages/kbn-plugin-generator/scripts/build.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const Path = require('path'); - -const { run } = require('@kbn/dev-utils'); -const del = require('del'); -const execa = require('execa'); - -run( - async ({ flags }) => { - await del(Path.resolve(__dirname, '../target')); - - await execa(require.resolve('typescript/bin/tsc'), flags.watch ? ['--watch'] : [], { - cwd: Path.resolve(__dirname, '..'), - stdio: 'inherit', - }); - }, - { - flags: { - boolean: ['watch'], - help: ` - --watch Watch files and rebuild on changes - `, - }, - } -); diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index 5e885527a76083b..9165fd21ebea0a7 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "target": "ES2019", "declaration": true, diff --git a/packages/kbn-rule-data-utils/jest.config.js b/packages/kbn-rule-data-utils/jest.config.js new file mode 100644 index 000000000000000..26cb39fe8b55ae8 --- /dev/null +++ b/packages/kbn-rule-data-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-rule-data-utils'], +}; diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json new file mode 100644 index 000000000000000..6f0b8439ec89158 --- /dev/null +++ b/packages/kbn-rule-data-utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/rule-data-utils", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + } +} diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts new file mode 100644 index 000000000000000..93a2538c7aa2c09 --- /dev/null +++ b/packages/kbn-rule-data-utils/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './technical_field_names'; diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts new file mode 100644 index 000000000000000..31779c9f08e819c --- /dev/null +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ValuesType } from 'utility-types'; + +const ALERT_NAMESPACE = 'kibana.rac.alert'; + +const TIMESTAMP = '@timestamp' as const; +const EVENT_KIND = 'event.kind' as const; +const EVENT_ACTION = 'event.action' as const; +const RULE_UUID = 'rule.uuid' as const; +const RULE_ID = 'rule.id' as const; +const RULE_NAME = 'rule.name' as const; +const RULE_CATEGORY = 'rule.category' as const; +const TAGS = 'tags' as const; +const PRODUCER = `${ALERT_NAMESPACE}.producer` as const; +const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; +const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const; +const ALERT_START = `${ALERT_NAMESPACE}.start` as const; +const ALERT_END = `${ALERT_NAMESPACE}.end` as const; +const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; +const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; +const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; +const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; +const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; +const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; + +const fields = { + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS, + PRODUCER, + ALERT_ID, + ALERT_UUID, + ALERT_START, + ALERT_END, + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_STATUS, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +}; + +export { + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS, + PRODUCER, + ALERT_ID, + ALERT_UUID, + ALERT_START, + ALERT_END, + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_STATUS, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +}; + +export type TechnicalRuleDataFieldName = ValuesType; diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json new file mode 100644 index 000000000000000..4b1262d11f3aff1 --- /dev/null +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-rule-data-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/kbn-securitysolution-es-utils/BUILD.bazel b/packages/kbn-securitysolution-es-utils/BUILD.bazel new file mode 100644 index 000000000000000..0cc27358c5da277 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/BUILD.bazel @@ -0,0 +1,86 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-es-utils" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-es-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "@npm//@elastic/elasticsearch", + "@npm//@hapi/hapi", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + srcs = SRCS, + args = ["--pretty"], + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", + deps = DEPS, +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-es-utils/README.md b/packages/kbn-securitysolution-es-utils/README.md new file mode 100644 index 000000000000000..b99aa095c84f474 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/README.md @@ -0,0 +1,6 @@ +# kbn-securitysolution-es-utils + +This is the shared security solution elastic search utilities among plugins. This was originally created +to remove the dependencies between security_solution and other projects such as lists. This should only be +used within server side code and not client side code since it is all elastic search utilities and packages. + diff --git a/packages/kbn-securitysolution-es-utils/jest.config.js b/packages/kbn-securitysolution-es-utils/jest.config.js new file mode 100644 index 000000000000000..6b86ec6e2da52b7 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-es-utils'], +}; diff --git a/packages/kbn-securitysolution-es-utils/package.json b/packages/kbn-securitysolution-es-utils/package.json new file mode 100644 index 000000000000000..7d0c0993c6c3263 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-es-utils", + "version": "1.0.0", + "description": "security solution elastic search utilities to use across plugins such lists, security_solution, cases, etc...", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-es-utils/src/bad_request_error/index.ts b/packages/kbn-securitysolution-es-utils/src/bad_request_error/index.ts new file mode 100644 index 000000000000000..525f6cfa5c9ffe1 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/bad_request_error/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class BadRequestError extends Error {} diff --git a/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts b/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts new file mode 100644 index 000000000000000..9671d35dc554e07 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/create_boostrap_index/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +// See the reference(s) below on explanations about why -000001 was chosen and +// why the is_write_index is true as well as the bootstrapping step which is needed. +// Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html +export const createBootstrapIndex = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + return ( + await esClient.transport.request({ + path: `/${index}-000001`, + method: 'PUT', + body: { + aliases: { + [index]: { + is_write_index: true, + }, + }, + }, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts new file mode 100644 index 000000000000000..4df4724aaf2b5ee --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/delete_all_index/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const deleteAllIndex = async ( + esClient: ElasticsearchClient, + pattern: string, + maxAttempts = 5 +): Promise => { + for (let attempt = 1; ; attempt++) { + if (attempt > maxAttempts) { + throw new Error( + `Failed to delete indexes with pattern [${pattern}] after ${maxAttempts} attempts` + ); + } + + // resolve pattern to concrete index names + const { body: resp } = await esClient.indices.getAlias( + { + index: pattern, + }, + { ignore: [404] } + ); + + // @ts-expect-error status doesn't exist on response + if (resp.status === 404) { + return true; + } + + const indices = Object.keys(resp) as string[]; + + // if no indexes exits then we're done with this pattern + if (!indices.length) { + return true; + } + + // delete the concrete indexes we found and try again until this pattern resolves to no indexes + await esClient.indices.delete({ + index: indices, + ignore_unavailable: true, + }); + } +}; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts new file mode 100644 index 000000000000000..34c1d2e5da45f84 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/delete_policy/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const deletePolicy = async ( + esClient: ElasticsearchClient, + policy: string +): Promise => { + return ( + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'DELETE', + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts b/packages/kbn-securitysolution-es-utils/src/delete_template/index.ts new file mode 100644 index 000000000000000..2e7a71af9f772bf --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/delete_template/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const deleteTemplate = async ( + esClient: ElasticsearchClient, + name: string +): Promise => { + return ( + await esClient.indices.deleteTemplate({ + name, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts b/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts new file mode 100644 index 000000000000000..0c2252bdc1f033c --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/elasticsearch_client/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Copied from src/core/server/elasticsearch/client/types.ts +// as these types aren't part of any package yet. Once they are, remove this completely + +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; +import type { + ApiResponse, + TransportRequestOptions, + TransportRequestParams, + TransportRequestPromise, +} from '@elastic/elasticsearch/lib/Transport'; + +/** + * Client used to query the elasticsearch cluster. + * @deprecated At some point use the one from src/core/server/elasticsearch/client/types.ts when it is made into a package. If it never is, then keep using this one. + * @public + */ +export type ElasticsearchClient = Omit< + KibanaClient, + 'connectionPool' | 'transport' | 'serializer' | 'extend' | 'child' | 'close' +> & { + transport: { + request( + params: TransportRequestParams, + options?: TransportRequestOptions + ): TransportRequestPromise; + }; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts new file mode 100644 index 000000000000000..885103c1fb584ae --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +interface AliasesResponse { + [indexName: string]: { + aliases: { + [aliasName: string]: { + is_write_index: boolean; + }; + }; + }; +} + +interface IndexAlias { + alias: string; + index: string; + isWriteIndex: boolean; +} + +/** + * Retrieves all index aliases for a given alias name + * + * @param esClient An {@link ElasticsearchClient} + * @param alias alias name used to filter results + * + * @returns an array of {@link IndexAlias} objects + */ +export const getIndexAliases = async ({ + esClient, + alias, +}: { + esClient: ElasticsearchClient; + alias: string; +}): Promise => { + const response = await esClient.indices.getAlias({ + name: alias, + }); + + return Object.keys(response.body).map((index) => ({ + alias, + index, + isWriteIndex: response.body[index].aliases[alias]?.is_write_index === true, + })); +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts new file mode 100644 index 000000000000000..523b41303a5691b --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +/** + * Retrieves the count of documents in a given index + * + * @param esClient An {@link ElasticsearchClient} + * @param index index whose documents will be counted + * + * @returns the document count + */ +export const getIndexCount = async ({ + esClient, + index, +}: { + esClient: ElasticsearchClient; + index: string; +}): Promise => { + const response = await esClient.count<{ count: number }>({ + index, + }); + + return response.body.count; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts new file mode 100644 index 000000000000000..b7d12cab3f48c12 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_index_exists/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const getIndexExists = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + try { + const { body: response } = await esClient.search({ + index, + size: 0, + allow_no_indices: true, + body: { + terminate_after: 1, + }, + }); + return response._shards.total > 0; + } catch (err) { + if (err.body != null && err.body.status === 404) { + return false; + } else { + throw err.body ? err.body : err; + } + } +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts new file mode 100644 index 000000000000000..cefd47dbe9d078d --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_policy_exists/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const getPolicyExists = async ( + esClient: ElasticsearchClient, + policy: string +): Promise => { + try { + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'GET', + }); + // Return true that there exists a policy which is not 404 or some error + // Since there is not a policy exists API, this is how we create one by calling + // into the API to get it if it exists or rely on it to throw a 404 + return true; + } catch (err) { + if (err.statusCode === 404) { + return false; + } else { + throw err; + } + } +}; diff --git a/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts b/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.ts new file mode 100644 index 000000000000000..c56c5b968d45c81 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/get_template_exists/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const getTemplateExists = async ( + esClient: ElasticsearchClient, + template: string +): Promise => { + return ( + await esClient.indices.existsTemplate({ + name: template, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/index.ts b/packages/kbn-securitysolution-es-utils/src/index.ts new file mode 100644 index 000000000000000..cfa6820e9aac526 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './bad_request_error'; +export * from './create_boostrap_index'; +export * from './delete_all_index'; +export * from './delete_policy'; +export * from './delete_template'; +export * from './elasticsearch_client'; +export * from './get_index_aliases'; +export * from './get_index_count'; +export * from './get_index_exists'; +export * from './get_policy_exists'; +export * from './get_template_exists'; +export * from './read_index'; +export * from './read_privileges'; +export * from './set_policy'; +export * from './set_template'; +export * from './transform_error'; diff --git a/packages/kbn-securitysolution-es-utils/src/read_index/index.ts b/packages/kbn-securitysolution-es-utils/src/read_index/index.ts new file mode 100644 index 000000000000000..cc16645120b7022 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/read_index/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const readIndex = async (esClient: ElasticsearchClient, index: string): Promise => { + return esClient.indices.get({ + index, + }); +}; diff --git a/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts new file mode 100644 index 000000000000000..8b11387a1d02085 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/read_privileges/index.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Copied from src/core/server/elasticsearch/legacy/api_types.ts including its deprecation mentioned below + * TODO: Remove this and refactor the readPrivileges to utilize any newer client side ways rather than all this deprecated legacy stuff + */ +export interface LegacyCallAPIOptions { + /** + * Indicates whether `401 Unauthorized` errors returned from the Elasticsearch API + * should be wrapped into `Boom` error instances with properly set `WWW-Authenticate` + * header that could have been returned by the API itself. If API didn't specify that + * then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. + */ + wrap401Errors?: boolean; + /** + * A signal object that allows you to abort the request via an AbortController object. + */ + signal?: AbortSignal; +} + +type CallWithRequest, V> = ( + endpoint: string, + params: T, + options?: LegacyCallAPIOptions +) => Promise; + +export const readPrivileges = async ( + callWithRequest: CallWithRequest<{}, unknown>, + index: string +): Promise => { + return callWithRequest('transport.request', { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: [ + 'all', + 'create_snapshot', + 'manage', + 'manage_api_key', + 'manage_ccr', + 'manage_transform', + 'manage_ilm', + 'manage_index_templates', + 'manage_ingest_pipelines', + 'manage_ml', + 'manage_own_api_key', + 'manage_pipeline', + 'manage_rollup', + 'manage_saml', + 'manage_security', + 'manage_token', + 'manage_watcher', + 'monitor', + 'monitor_transform', + 'monitor_ml', + 'monitor_rollup', + 'monitor_watcher', + 'read_ccr', + 'read_ilm', + 'transport_client', + ], + index: [ + { + names: [index], + privileges: [ + 'all', + 'create', + 'create_doc', + 'create_index', + 'delete', + 'delete_index', + 'index', + 'manage', + 'maintenance', + 'manage_follow_index', + 'manage_ilm', + 'manage_leader_index', + 'monitor', + 'read', + 'read_cross_cluster', + 'view_index_metadata', + 'write', + ], + }, + ], + }, + }); +}; diff --git a/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts b/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts new file mode 100644 index 000000000000000..dc45ca3e1c0894c --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/set_policy/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const setPolicy = async ( + esClient: ElasticsearchClient, + policy: string, + body: Record +): Promise => { + return ( + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'PUT', + body, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/set_template/index.ts b/packages/kbn-securitysolution-es-utils/src/set_template/index.ts new file mode 100644 index 000000000000000..89aaa44f29e0d0a --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/set_template/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '../elasticsearch_client'; + +export const setTemplate = async ( + esClient: ElasticsearchClient, + name: string, + body: Record +): Promise => { + return ( + await esClient.indices.putTemplate({ + name, + body, + }) + ).body; +}; diff --git a/packages/kbn-securitysolution-es-utils/src/transform_error/index.test.ts b/packages/kbn-securitysolution-es-utils/src/transform_error/index.test.ts new file mode 100644 index 000000000000000..e0f520f1ebfd4f4 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/transform_error/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Boom from '@hapi/boom'; +import { transformError } from '.'; +import { BadRequestError } from '../bad_request_error'; +import { errors } from '@elastic/elasticsearch'; + +describe('transformError', () => { + test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { + const boom = new Boom.Boom('some boom message'); + const transformed = transformError(boom); + expect(transformed).toEqual({ + message: 'An internal server error occurred', + statusCode: 500, + }); + }); + + test('returns transformed output if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); + }); + + test('returns a transformed message with the message set and statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); + }); + + test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { + const error: Error = { + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 500, + }); + }); + + test('it detects a BadRequestError and returns a status code of 400 from that particular error type', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); + }); + + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); + }); + + it('transforms a ResponseError returned by the elasticsearch client', () => { + const error: errors.ResponseError = { + name: 'ResponseError', + message: 'illegal_argument_exception', + headers: {}, + body: { + error: { + type: 'illegal_argument_exception', + reason: 'detailed explanation', + }, + }, + meta: ({} as unknown) as errors.ResponseError['meta'], + statusCode: 400, + }; + const transformed = transformError(error); + + expect(transformed).toEqual({ + message: 'illegal_argument_exception: detailed explanation', + statusCode: 400, + }); + }); +}); diff --git a/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts b/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts new file mode 100644 index 000000000000000..b532dc5d1b6d070 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/src/transform_error/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; +import { BadRequestError } from '../bad_request_error'; + +export interface OutputError { + message: string; + statusCode: number; +} + +export const transformError = (err: Error & Partial): OutputError => { + if (Boom.isBoom(err)) { + return { + message: err.output.payload.message, + statusCode: err.output.statusCode, + }; + } else { + if (err.statusCode != null) { + if (err.body != null && err.body.error != null) { + return { + statusCode: err.statusCode, + message: `${err.body.error.type}: ${err.body.error.reason}`, + }; + } else { + return { + statusCode: err.statusCode, + message: err.message, + }; + } + } else if (err instanceof BadRequestError) { + // allows us to throw request validation errors in the absence of Boom + return { + message: err.message, + statusCode: 400, + }; + } else { + // natively return the err and allow the regular framework + // to deal with the error when it is a non Boom + return { + message: err.message != null ? err.message : '(unknown error message)', + statusCode: 500, + }; + } + } +}; diff --git a/packages/kbn-securitysolution-es-utils/tsconfig.json b/packages/kbn-securitysolution-es-utils/tsconfig.json new file mode 100644 index 000000000000000..be8848d781caea5 --- /dev/null +++ b/packages/kbn-securitysolution-es-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-es-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel new file mode 100644 index 000000000000000..ba7123d0c1f21a0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-alerting-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-alerting-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/README.md b/packages/kbn-securitysolution-io-ts-alerting-types/README.md new file mode 100644 index 000000000000000..b8fa8234f2d85f6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-alerting-types + +Types that are specific to the security solution alerting to be shared among plugins. + +Related packages are +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js new file mode 100644 index 000000000000000..6125b95a9bce580 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-alerting-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/package.json b/packages/kbn-securitysolution-io-ts-alerting-types/package.json new file mode 100644 index 000000000000000..ac972e06c1dc90e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-alerting-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts new file mode 100644 index 000000000000000..f0fe7f44a6f3e5e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultExportFileName } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_export_file_name', () => { + test('it should validate a regular string', () => { + const payload = 'some string'; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultExportFileName"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "export.ndjson"', () => { + const payload = null; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('export.ndjson'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts new file mode 100644 index 000000000000000..ccfb7923a230c51 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultFromString } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_from_string', () => { + test('it should validate a from string', () => { + const payload = 'now-20m'; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultFromString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "now-6m"', () => { + const payload = null; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('now-6m'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts new file mode 100644 index 000000000000000..f5706677e6c5d88 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultIntervalString } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_interval_string', () => { + test('it should validate a interval string', () => { + const payload = '20m'; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultIntervalString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "5m"', () => { + const payload = null; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('5m'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts new file mode 100644 index 000000000000000..82bd8607dae727b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { Language } from '../language'; +import { DefaultLanguageString } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_language_string', () => { + test('it should validate a string', () => { + const payload: Language = 'lucene'; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultLanguageString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "kuery"', () => { + const payload = null; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('kuery'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts new file mode 100644 index 000000000000000..eb2af1dbea41a07 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultMaxSignalsNumber } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { DEFAULT_MAX_SIGNALS } from '../constants'; + +describe('default_from_string', () => { + test('it should validate a max signal number', () => { + const payload = 5; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultMaxSignals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultMaxSignals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultMaxSignals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of DEFAULT_MAX_SIGNALS', () => { + const payload = null; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts new file mode 100644 index 000000000000000..cca1c7e2774f454 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultPage } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_page', () => { + test('it should validate a regular number greater than zero', () => { + const payload = 5; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a string of a number', () => { + const payload = '5'; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(5); + }); + + test('it should not validate a junk string', () => { + const payload = 'invalid-string'; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 20', () => { + const payload = null; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts new file mode 100644 index 000000000000000..f9140be68ec8d78 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; + +/** + * Types the DefaultPerPage as: + * - If a string this will convert the string to a number + * - If null or undefined, then a default of 1 will be used + * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + */ +export const DefaultPage = new t.Type( + 'DefaultPerPage', + t.number.is, + (input, context): Either => { + if (input == null) { + return t.success(1); + } else if (typeof input === 'string') { + return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); + } else { + return PositiveIntegerGreaterThanZero.validate(input, context); + } + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts new file mode 100644 index 000000000000000..88e91986a65dd87 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultPerPage } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_per_page', () => { + test('it should validate a regular number greater than zero', () => { + const payload = 5; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a string of a number', () => { + const payload = '5'; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(5); + }); + + test('it should not validate a junk string', () => { + const payload = 'invalid-string'; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 20', () => { + const payload = null; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(20); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts new file mode 100644 index 000000000000000..ea8f30c74506296 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; + +/** + * Types the DefaultPerPage as: + * - If a string this will convert the string to a number + * - If null or undefined, then a default of 20 will be used + * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + */ +export const DefaultPerPage = new t.Type( + 'DefaultPerPage', + t.number.is, + (input, context): Either => { + if (input == null) { + return t.success(20); + } else if (typeof input === 'string') { + return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); + } else { + return PositiveIntegerGreaterThanZero.validate(input, context); + } + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts new file mode 100644 index 000000000000000..5f1ef3fc61fab2e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { Threats } from '../threat'; +import { DefaultThreatArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_threat_null', () => { + test('it should validate an empty array', () => { + const payload: Threats = []; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of threats', () => { + const payload: Threats = [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ]; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + 5, + ]; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultThreatArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default empty array if not provided a value', () => { + const payload = null; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts new file mode 100644 index 000000000000000..b92815d4fe828e1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { Throttle } from '../throttle'; +import { DefaultThrottleNull } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_throttle_null', () => { + test('it should validate a throttle string', () => { + const payload: Throttle = 'some string'; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = 5; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultThreatNull"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default "null" if not provided a value', () => { + const payload = undefined; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(null); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts new file mode 100644 index 000000000000000..31c35c8319fab88 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultToString } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_to_string', () => { + test('it should validate a to string', () => { + const payload = 'now-5m'; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultToString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "now"', () => { + const payload = null; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('now'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts new file mode 100644 index 000000000000000..c471141a99a7636 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultUuid } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts new file mode 100644 index 000000000000000..73bf807e92c433f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +/** + * Types the DefaultUuid as: + * - If null or undefined, then a default string uuid.v4() will be + * created otherwise it will be checked just against an empty string + */ +export const DefaultUuid = new t.Type( + 'DefaultUuid', + t.string.is, + (input, context): Either => + input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts new file mode 100644 index 000000000000000..37ed4b2daa51001 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Either } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-types'; + +const stringValidator = (input: unknown): input is string => typeof input === 'string'; + +export const from = new t.Type( + 'From', + t.string.is, + (input, context): Either => { + if (stringValidator(input) && parseScheduleDates(input) == null) { + return t.failure(input, context, 'Failed to parse "from" on rule param'); + } + return t.string.validate(input, context); + }, + t.identity +); +export type From = t.TypeOf; + +export const fromOrUndefined = t.union([from, t.undefined]); +export type FromOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts new file mode 100644 index 000000000000000..c6f29862206e6b3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './actions'; +export * from './constants'; +export * from './default_actions_array'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_risk_score_mapping_array'; +export * from './default_severity_mapping_array'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './from'; +export * from './language'; +export * from './machine_learning_job_id'; +export * from './max_signals'; +export * from './normalized_ml_job_id'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './risk_score_mapping'; +export * from './saved_object_attributes'; +export * from './severity'; +export * from './severity_mapping'; +export * from './threat'; +export * from './threat_mapping'; +export * from './threat_subtechnique'; +export * from './threat_tactic'; +export * from './threat_technique'; +export * from './throttle'; +export * from './type'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts new file mode 100644 index 000000000000000..0632f09e6a393ab --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const language = t.keyof({ eql: null, kuery: null, lucene: null }); +export type Language = t.TypeOf; + +export const languageOrUndefined = t.union([language, t.undefined]); +export type LanguageOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts new file mode 100644 index 000000000000000..9e9c25c62b938b7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +import { machine_learning_job_id_normalized } from '../normalized_ml_job_id'; + +export const machine_learning_job_id = t.union([t.string, machine_learning_job_id_normalized]); +export type MachineLearningJobId = t.TypeOf; + +export const machineLearningJobIdOrUndefined = t.union([machine_learning_job_id, t.undefined]); +export type MachineLearningJobIdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts new file mode 100644 index 000000000000000..ef7a225d93733d7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; + +export const max_signals = PositiveIntegerGreaterThanZero; +export type MaxSignals = t.TypeOf; + +export const maxSignalsOrUndefined = t.union([max_signals, t.undefined]); +export type MaxSignalsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts new file mode 100644 index 000000000000000..db26264c029cddb --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +import { NonEmptyArray } from '@kbn/securitysolution-io-ts-types'; + +export const machine_learning_job_id_normalized = NonEmptyArray(t.string); +export type MachineLearningJobIdNormalized = t.TypeOf; + +export const machineLearningJobIdNormalizedOrUndefined = t.union([ + machine_learning_job_id_normalized, + t.undefined, +]); +export type MachineLearningJobIdNormalizedOrUndefined = t.TypeOf< + typeof machineLearningJobIdNormalizedOrUndefined +>; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts new file mode 100644 index 000000000000000..38fd27ac40fdf85 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { ReferencesDefaultArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_string_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = ReferencesDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of strings', () => { + const payload = ['value 1', 'value 2']; + const decoded = ReferencesDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['value 1', 5]; + const decoded = ReferencesDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "referencesWithDefaultArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = ReferencesDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts new file mode 100644 index 000000000000000..d341ca8b3b4f784 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { RiskScore } from '.'; + +describe('risk_score', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a zero', () => { + const payload = 0; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "RiskScore"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "RiskScore"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a risk score greater than 100', () => { + const payload = 101; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "101" supplied to "RiskScore"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts new file mode 100644 index 000000000000000..98b9c33e7e3ea52 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the risk score as: + * - Natural Number (positive integer and not a float), + * - Between the values [0 and 100] inclusive. + */ +export const RiskScore = new t.Type( + 'RiskScore', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 && input <= 100 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); + +export type RiskScoreC = typeof RiskScore; + +export const risk_score = RiskScore; +export type RiskScore = t.TypeOf; + +export const riskScoreOrUndefined = t.union([risk_score, t.undefined]); +export type RiskScoreOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts new file mode 100644 index 000000000000000..be07bab64f4698e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { operator } from '@kbn/securitysolution-io-ts-types'; +import { riskScoreOrUndefined } from '../risk_score'; + +export const risk_score_mapping_field = t.string; +export const risk_score_mapping_value = t.string; +export const risk_score_mapping_item = t.exact( + t.type({ + field: risk_score_mapping_field, + value: risk_score_mapping_value, + operator, + risk_score: riskScoreOrUndefined, + }) +); + +export const risk_score_mapping = t.array(risk_score_mapping_item); +export type RiskScoreMapping = t.TypeOf; + +export const riskScoreMappingOrUndefined = t.union([risk_score_mapping, t.undefined]); +export type RiskScoreMappingOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts new file mode 100644 index 000000000000000..1a3fd50039c29b2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +import { operator } from '@kbn/securitysolution-io-ts-types'; +import { severity } from '../severity'; + +export const severity_mapping_field = t.string; +export const severity_mapping_value = t.string; +export const severity_mapping_item = t.exact( + t.type({ + field: severity_mapping_field, + operator, + value: severity_mapping_value, + severity, + }) +); +export type SeverityMappingItem = t.TypeOf; + +export const severity_mapping = t.array(severity_mapping_item); +export type SeverityMapping = t.TypeOf; + +export const severityMappingOrUndefined = t.union([severity_mapping, t.undefined]); +export type SeverityMappingOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts new file mode 100644 index 000000000000000..08ff6cca60a499f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { threat_tactic } from '../threat_tactic'; +import { threat_techniques } from '../threat_technique'; + +export const threat_framework = t.string; + +export const threat = t.intersection([ + t.exact( + t.type({ + framework: threat_framework, + tactic: threat_tactic, + }) + ), + t.exact( + t.partial({ + technique: threat_techniques, + }) + ), +]); + +export type Threat = t.TypeOf; + +export const threats = t.array(threat); +export type Threats = t.TypeOf; + +export const threatsOrUndefined = t.union([threats, t.undefined]); +export type ThreatsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts new file mode 100644 index 000000000000000..16fd1647e5bfc61 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { + concurrent_searches, + items_per_search, + ThreatMapping, + threatMappingEntries, + ThreatMappingEntries, + threat_mapping, +} from '.'; +import { foldLeftRight, getPaths, exactCheck } from '@kbn/securitysolution-io-ts-utils'; + +describe('threat_mapping', () => { + describe('threatMappingEntries', () => { + test('it should validate an entry', () => { + const payload: ThreatMappingEntries = [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation with an extra entry item', () => { + const payload: ThreatMappingEntries & Array<{ extra: string }> = [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + extra: 'blah', + }, + ]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation with a non string', () => { + const payload = ([ + { + field: 5, + type: 'mapping', + value: 'field.one', + }, + ] as unknown) as ThreatMappingEntries[]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation with a wrong type', () => { + const payload = ([ + { + field: 'field.one', + type: 'invalid', + value: 'field.one', + }, + ] as unknown) as ThreatMappingEntries[]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('threat_mapping', () => { + test('it should validate a threat mapping', () => { + const payload: ThreatMapping = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ], + }, + ]; + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + test('it should fail validate with an extra key', () => { + const payload: ThreatMapping & Array<{ extra: string }> = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ], + extra: 'invalid', + }, + ]; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validate with an extra inner entry', () => { + const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + extra: 'blah', + }, + ], + }, + ]; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validate with an extra inner entry with the wrong data type', () => { + const payload = ([ + { + entries: [ + { + field: 5, + type: 'mapping', + value: 'field.one', + }, + ], + }, + ] as unknown) as ThreatMapping; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "entries,field"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validate with empty array', () => { + const payload: string[] = []; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when concurrent_searches is < 0', () => { + const payload = -1; + const decoded = concurrent_searches.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when concurrent_searches is 0', () => { + const payload = 0; + const decoded = concurrent_searches.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when items_per_search is 0', () => { + const payload = 0; + const decoded = items_per_search.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when items_per_search is < 0', () => { + const payload = -1; + const decoded = items_per_search.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts new file mode 100644 index 000000000000000..abee0d2baceb06f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { + NonEmptyArray, + NonEmptyString, + PositiveIntegerGreaterThanZero, +} from '@kbn/securitysolution-io-ts-types'; +import { language } from '../language'; + +export const threat_query = t.string; +export type ThreatQuery = t.TypeOf; +export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); +export type ThreatQueryOrUndefined = t.TypeOf; + +export const threat_indicator_path = t.string; +export type ThreatIndicatorPath = t.TypeOf; +export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); +export type ThreatIndicatorPathOrUndefined = t.TypeOf; + +export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet +export type ThreatFilters = t.TypeOf; +export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); +export type ThreatFiltersOrUndefined = t.TypeOf; + +export const threatMapEntry = t.exact( + t.type({ + field: NonEmptyString, + type: t.keyof({ mapping: null }), + value: NonEmptyString, + }) +); + +export type ThreatMapEntry = t.TypeOf; + +export const threatMappingEntries = t.array(threatMapEntry); +export type ThreatMappingEntries = t.TypeOf; + +export const threatMap = t.exact( + t.type({ + entries: threatMappingEntries, + }) +); +export type ThreatMap = t.TypeOf; + +export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); +export type ThreatMapping = t.TypeOf; + +export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); +export type ThreatMappingOrUndefined = t.TypeOf; + +export const threat_index = t.array(t.string); +export type ThreatIndex = t.TypeOf; +export const threatIndexOrUndefined = t.union([threat_index, t.undefined]); +export type ThreatIndexOrUndefined = t.TypeOf; + +export const threat_language = t.union([language, t.undefined]); +export type ThreatLanguage = t.TypeOf; +export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]); +export type ThreatLanguageOrUndefined = t.TypeOf; + +export const concurrent_searches = PositiveIntegerGreaterThanZero; +export type ConcurrentSearches = t.TypeOf; +export const concurrentSearchesOrUndefined = t.union([concurrent_searches, t.undefined]); +export type ConcurrentSearchesOrUndefined = t.TypeOf; + +export const items_per_search = PositiveIntegerGreaterThanZero; +export type ItemsPerSearch = t.TypeOf; +export const itemsPerSearchOrUndefined = t.union([items_per_search, t.undefined]); +export type ItemsPerSearchOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts new file mode 100644 index 000000000000000..4909b82d8ec5403 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const threat_subtechnique_id = t.string; +export const threat_subtechnique_name = t.string; +export const threat_subtechnique_reference = t.string; + +export const threat_subtechnique = t.type({ + id: threat_subtechnique_id, + name: threat_subtechnique_name, + reference: threat_subtechnique_reference, +}); + +export const threat_subtechniques = t.array(threat_subtechnique); + +export type ThreatSubtechnique = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts new file mode 100644 index 000000000000000..2d56e842287d87a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { threat_subtechniques } from '../threat_subtechnique'; + +export const threat_technique_id = t.string; +export const threat_technique_name = t.string; +export const threat_technique_reference = t.string; + +export const threat_technique = t.intersection([ + t.exact( + t.type({ + id: threat_technique_id, + name: threat_technique_name, + reference: threat_technique_reference, + }) + ), + t.exact( + t.partial({ + subtechnique: threat_subtechniques, + }) + ), +]); +export const threat_techniques = t.array(threat_technique); + +export type ThreatTechnique = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts new file mode 100644 index 000000000000000..0e740378789920e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const type = t.keyof({ + eql: null, + machine_learning: null, + query: null, + saved_query: null, + threshold: null, + threat_match: null, +}); +export type Type = t.TypeOf; + +export const typeOrUndefined = t.union([type, t.undefined]); +export type TypeOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json new file mode 100644 index 000000000000000..3411ce2c93d0531 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-alerting-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel new file mode 100644 index 000000000000000..e9b806288adddb7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-list-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-list-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-list-types/README.md b/packages/kbn-securitysolution-io-ts-list-types/README.md new file mode 100644 index 000000000000000..090ede2ed7d625f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-list-types + +io-ts types that are specific to lists to be shared among plugins + +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-ts-utils +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-list-types/jest.config.js b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js new file mode 100644 index 000000000000000..0312733b6a02bc5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-list-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/package.json b/packages/kbn-securitysolution-io-ts-list-types/package.json new file mode 100644 index 000000000000000..74893e59855bc4e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-list-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts new file mode 100644 index 000000000000000..380f7f13b621061 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Comment, CommentsArray } from '.'; +import { DATE_NOW, ID, USER } from '../constants/index.mock'; + +export const getCommentsMock = (): Comment => ({ + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, + id: ID, +}); + +export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()]; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts new file mode 100644 index 000000000000000..89e734a92fd04e3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getCommentsArrayMock, getCommentsMock } from './index.mock'; +import { + Comment, + comment, + CommentsArray, + commentsArray, + CommentsArrayOrUndefined, + commentsArrayOrUndefined, +} from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { DATE_NOW } from '../constants/index.mock'; + +describe('Comment', () => { + describe('comment', () => { + test('it fails validation when "id" is undefined', () => { + const payload = { ...getCommentsMock(), id: undefined }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it passes validation with a typical comment', () => { + const payload = getCommentsMock(); + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation with "updated_at" and "updated_by" fields included', () => { + const payload = getCommentsMock(); + payload.updated_at = DATE_NOW; + payload.updated_by = 'someone'; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is an empty string', () => { + const payload: Omit & { comment: string } = { + ...getCommentsMock(), + comment: '', + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCommentsMock(), + comment: ['some value'], + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "created_at" is not a string', () => { + const payload: Omit & { created_at: number } = { + ...getCommentsMock(), + created_at: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "created_by" is not a string', () => { + const payload: Omit & { created_by: number } = { + ...getCommentsMock(), + created_by: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "created_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "updated_at" is not a string', () => { + const payload: Omit & { updated_at: number } = { + ...getCommentsMock(), + updated_at: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "updated_by" is not a string', () => { + const payload: Omit & { updated_by: number } = { + ...getCommentsMock(), + updated_by: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: Comment & { + extraKey?: string; + } = getCommentsMock(); + payload.extraKey = 'some value'; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCommentsMock()); + }); + }); + + describe('commentsArray', () => { + test('it passes validation an array of Comment', () => { + const payload = getCommentsArrayMock(); + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation when a Comment includes "updated_at" and "updated_by"', () => { + const commentsPayload = getCommentsMock(); + commentsPayload.updated_at = DATE_NOW; + commentsPayload.updated_by = 'someone'; + const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()]; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when array includes non Comment types', () => { + const payload = ([1] as unknown) as CommentsArray; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('commentsArrayOrUndefined', () => { + test('it passes validation an array of Comment', () => { + const payload = getCommentsArrayMock(); + const decoded = commentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation when undefined', () => { + const payload = undefined; + const decoded = commentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when array includes non Comment types', () => { + const payload = ([1] as unknown) as CommentsArrayOrUndefined; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts new file mode 100644 index 000000000000000..3b8cc6cc6ce95c2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { created_at } from '../created_at'; +import { created_by } from '../created_by'; +import { id } from '../id'; +import { updated_at } from '../updated_at'; +import { updated_by } from '../updated_by'; + +export const comment = t.intersection([ + t.exact( + t.type({ + comment: NonEmptyString, + created_at, + created_by, + id, + }) + ), + t.exact( + t.partial({ + updated_at, + updated_by, + }) + ), +]); + +export const commentsArray = t.array(comment); +export type CommentsArray = t.TypeOf; +export type Comment = t.TypeOf; +export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); +export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts new file mode 100644 index 000000000000000..d2107ae864f15a5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export const ENTRY_VALUE = 'some host name'; +export const FIELD = 'host.name'; +export const MATCH = 'match'; +export const MATCH_ANY = 'match_any'; +export const OPERATOR = 'included'; +export const NESTED = 'nested'; +export const NESTED_FIELD = 'parent.field'; +export const LIST_ID = 'some-list-id'; +export const LIST = 'list'; +export const TYPE = 'ip'; +export const EXISTS = 'exists'; +export const WILDCARD = 'wildcard'; +export const USER = 'some user'; +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; + +// Exception List specific +export const ID = 'uuid_here'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts new file mode 100644 index 000000000000000..f86986fc328c586 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list. + * + * TODO: Create a kbn-securitysolution-constants and add this to it. + * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts new file mode 100644 index 000000000000000..3baf0054221db63 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getCreateCommentsArrayMock, getCreateCommentsMock } from './index.mock'; +import { + CreateComment, + createComment, + CreateCommentsArray, + createCommentsArray, + CreateCommentsArrayOrUndefined, + createCommentsArrayOrUndefined, +} from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('CreateComment', () => { + describe('createComment', () => { + test('it passes validation with a default comment', () => { + const payload = getCreateCommentsMock(); + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "{| comment: NonEmptyString |}"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCreateCommentsMock(), + comment: ['some value'], + }; + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: CreateComment & { + extraKey?: string; + } = getCreateCommentsMock(); + payload.extraKey = 'some value'; + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCreateCommentsMock()); + }); + }); + + describe('createCommentsArray', () => { + test('it passes validation an array of comments', () => { + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<{| comment: NonEmptyString |}>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when array includes non comments types', () => { + const payload = ([1] as unknown) as CreateCommentsArray; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('createCommentsArrayOrUndefined', () => { + test('it passes validation an array of comments', () => { + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation when undefined', () => { + const payload = undefined; + const decoded = createCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when array includes non comments types', () => { + const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts new file mode 100644 index 000000000000000..883675ce51f919a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +export const createComment = t.exact( + t.type({ + comment: NonEmptyString, + }) +); + +export type CreateComment = t.TypeOf; +export const createCommentsArray = t.array(createComment); +export type CreateCommentsArray = t.TypeOf; +export type CreateComments = t.TypeOf; +export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); +export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts new file mode 100644 index 000000000000000..440c60187668272 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { CommentsArray } from '../comment'; +import { DefaultCommentsArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getCommentsArrayMock } from '../comment/index.mock'; + +describe('default_comments_array', () => { + test('it should pass validation when supplied an empty array', () => { + const payload: CommentsArray = []; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied an array of comments', () => { + const payload: CommentsArray = getCommentsArrayMock(); + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an array of numbers', () => { + const payload = [1]; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts new file mode 100644 index 000000000000000..de45fd9f300fac3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '../comment'; +import { DefaultCommentsArray } from '../default_comments_array'; +import { getCommentsArrayMock } from '../comment/index.mock'; + +describe('default_comments_array', () => { + test('it should pass validation when supplied an empty array', () => { + const payload: CommentsArray = []; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied an array of comments', () => { + const payload: CommentsArray = getCommentsArrayMock(); + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an array of numbers', () => { + const payload = [1]; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts new file mode 100644 index 000000000000000..21e8c375b3d01bc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultNamespace } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_namespace', () => { + test('it should validate "single"', () => { + const payload = 'single'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate "agnostic"', () => { + const payload = 'agnostic'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it defaults to "single" if "undefined"', () => { + const payload = undefined; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it defaults to "single" if "null"', () => { + const payload = null; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it should FAIL validation if not "single" or "agnostic"', () => { + const payload = 'something else'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + `Invalid value "something else" supplied to "DefaultNamespace"`, + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts new file mode 100644 index 000000000000000..b02a3b96a5a3d36 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_namespace_array', () => { + test('it should validate "null" single item as an array with a "single" value', () => { + const payload: DefaultNamespaceArrayType = null; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single']); + }); + + test('it should FAIL validation of numeric value', () => { + const payload = 5; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultNamespaceArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate "undefined" item as an array with a "single" value', () => { + const payload: DefaultNamespaceArrayType = undefined; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single']); + }); + + test('it should validate "single" as an array of a "single" value', () => { + const payload: DefaultNamespaceArrayType = 'single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([payload]); + }); + + test('it should validate "agnostic" as an array of a "agnostic" value', () => { + const payload: DefaultNamespaceArrayType = 'agnostic'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([payload]); + }); + + test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => { + const payload: DefaultNamespaceArrayType = 'agnostic,single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['agnostic', 'single']); + }); + + test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => { + const payload: DefaultNamespaceArrayType = 'single,agnostic,single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single', 'agnostic', 'single']); + }); + + test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => { + const payload: DefaultNamespaceArrayType = ' single, agnostic, single '; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single', 'agnostic', 'single']); + }); + + test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "junk" supplied to "DefaultNamespaceArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts new file mode 100644 index 000000000000000..fa6613538b18ece --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { UpdateCommentsArray } from '../update_comment'; +import { DefaultUpdateCommentsArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getUpdateCommentsArrayMock } from '../update_comment/index.mock'; + +describe('default_update_comments_array', () => { + test('it should pass validation when supplied an empty array', () => { + const payload: UpdateCommentsArray = []; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied an array of comments', () => { + const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an array of numbers', () => { + const payload = [1]; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/description/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts new file mode 100644 index 000000000000000..09f1740567bc12c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { + endpointEntriesArray, + nonEmptyEndpointEntriesArray, + NonEmptyEndpointEntriesArray, +} from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; +import { getEndpointEntriesArrayMock } from './index.mock'; +import { getEntryListMock } from '../../entries_list/index.mock'; +import { getEntryExistsMock } from '../../entries_exist/index.mock'; + +describe('Endpoint', () => { + describe('entriesArray', () => { + test('it should validate an array with match entry', () => { + const payload = [getEndpointEntryMatchMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with match_any entry', () => { + const payload = [getEndpointEntryMatchAnyMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an empty array', () => { + const payload: NonEmptyEndpointEntriesArray = []; + const decoded = nonEmptyEndpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyEndpointEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { + const payload: NonEmptyEndpointEntriesArray = [getEndpointEntryMatchAnyMock()]; + const guarded = nonEmptyEndpointEntriesArray.is(payload); + expect(guarded).toBeTruthy(); + }); + + test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { + const payload: NonEmptyEndpointEntriesArray = []; + const guarded = nonEmptyEndpointEntriesArray.is(payload); + expect(guarded).toBeFalsy(); + }); + + test('it should NOT validate an array with exists entry', () => { + const payload = [getEntryExistsMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "exists" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + 'Invalid value "undefined" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array with list entry', () => { + const payload = [getEntryListMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "list" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + 'Invalid value "undefined" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array with nested entry', () => { + const payload = [getEndpointEntryNestedMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with all types of entries', () => { + const payload = getEndpointEntriesArrayMock(); + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts new file mode 100644 index 000000000000000..17a1a083d73d873 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EndpointEntryMatch } from '.'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; + +export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH, + value: ENTRY_VALUE, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts new file mode 100644 index 000000000000000..fc3a2dded177d5e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchMock } from './index.mock'; +import { EndpointEntryMatch, endpointEntryMatch } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchMock } from '../../entry_match/index.mock'; + +describe('endpointEntryMatch', () => { + test('it should validate an entry', () => { + const payload = getEndpointEntryMatchMock(); + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate when "operator" is "excluded"', () => { + // Use the generic entry mock so we can test operator: excluded + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "excluded" supplied to "operator"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEndpointEntryMatchMock(), + field: '', + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEndpointEntryMatchMock(), + value: ['some value'], + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEndpointEntryMatchMock(), + value: '', + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match"', () => { + const payload: Omit & { type: string } = { + ...getEndpointEntryMatchMock(), + type: 'match_any', + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryMatch & { + extraKey?: string; + } = getEndpointEntryMatchMock(); + payload.extraKey = 'some value'; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts new file mode 100644 index 000000000000000..07a1fc58a3d54a0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; + +export const endpointEntryMatch = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ match: null }), + value: NonEmptyString, + }) +); +export type EndpointEntryMatch = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts new file mode 100644 index 000000000000000..13fb16d73457da4 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; +import { EndpointEntryMatchAny } from '.'; + +export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH_ANY, + value: [ENTRY_VALUE], +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts new file mode 100644 index 000000000000000..cf646477725196f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchAnyMock } from './index.mock'; +import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock'; + +describe('endpointEntryMatchAny', () => { + test('it should validate an entry', () => { + const payload = getEndpointEntryMatchAnyMock(); + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate when operator is "excluded"', () => { + // Use the generic entry mock so we can test operator: excluded + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "excluded" supplied to "operator"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when field is empty string', () => { + const payload: Omit & { field: string } = { + ...getEndpointEntryMatchAnyMock(), + field: '', + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is empty array', () => { + const payload: Omit & { value: string[] } = { + ...getEndpointEntryMatchAnyMock(), + value: [], + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is not string array', () => { + const payload: Omit & { value: string } = { + ...getEndpointEntryMatchAnyMock(), + value: 'some string', + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match_any"', () => { + const payload: Omit & { type: string } = { + ...getEndpointEntryMatchAnyMock(), + type: 'match', + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryMatchAny & { + extraKey?: string; + } = getEndpointEntryMatchAnyMock(); + payload.extraKey = 'some extra key'; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchAnyMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts new file mode 100644 index 000000000000000..23c15767a511c7c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { + NonEmptyString, + nonEmptyOrNullableStringArray, + operatorIncluded, +} from '@kbn/securitysolution-io-ts-types'; + +export const endpointEntryMatchAny = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ match_any: null }), + value: nonEmptyOrNullableStringArray, + }) +); +export type EndpointEntryMatchAny = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts new file mode 100644 index 000000000000000..2697f3edc3db4fe --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; + +export const endpointEntryMatchWildcard = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ wildcard: null }), + value: NonEmptyString, + }) +); +export type EndpointEntryMatchWildcard = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts new file mode 100644 index 000000000000000..31d983ba58fe3e7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EndpointEntryNested } from '.'; +import { FIELD, NESTED } from '../../constants/index.mock'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; + +export const getEndpointEntryNestedMock = (): EndpointEntryNested => ({ + entries: [getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock()], + field: FIELD, + type: NESTED, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts new file mode 100644 index 000000000000000..f8e54e495652707 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EndpointEntryNested, endpointEntryNested } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEndpointEntryNestedMock } from './index.mock'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { + nonEmptyEndpointNestedEntriesArray, + NonEmptyEndpointNestedEntriesArray, +} from '../non_empty_nested_entries_array'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryExistsMock } from '../../entries_exist/index.mock'; + +describe('endpointEntryNested', () => { + test('it should validate a nested entry', () => { + const payload = getEndpointEntryNestedMock(); + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "type" is not "nested"', () => { + const payload: Omit & { type: 'match' } = { + ...getEndpointEntryNestedMock(), + type: 'match', + }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { + field: string; + } = { ...getEndpointEntryNestedMock(), field: '' }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is not a string', () => { + const payload: Omit & { + field: number; + } = { ...getEndpointEntryNestedMock(), field: 1 }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "entries" is not an array', () => { + const payload: Omit & { + entries: string; + } = { ...getEndpointEntryNestedMock(), entries: 'im a string' }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "im a string" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate when "entries" contains an entry item that is type "match"', () => { + const payload = { ...getEndpointEntryNestedMock(), entries: [getEndpointEntryMatchAnyMock()] }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host name'], + }, + ], + field: 'host.name', + type: 'nested', + }); + }); + + test('it should NOT validate when "entries" contains an entry item that is type "exists"', () => { + const payload = { ...getEndpointEntryNestedMock(), entries: [getEntryExistsMock()] }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "exists" supplied to "entries,type"', + 'Invalid value "undefined" supplied to "entries,value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryNested & { + extraKey?: string; + } = getEndpointEntryNestedMock(); + payload.extraKey = 'some extra key'; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEndpointEntryNestedMock()); + }); + + test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { + const payload: NonEmptyEndpointNestedEntriesArray = [ + getEndpointEntryMatchMock(), + getEndpointEntryMatchAnyMock(), + ]; + const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); + expect(guarded).toBeTruthy(); + }); + + test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { + const payload: NonEmptyEndpointNestedEntriesArray = []; + const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); + expect(guarded).toBeFalsy(); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts new file mode 100644 index 000000000000000..bd4c90d851a9019 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array'; + +export const endpointEntryNested = t.exact( + t.type({ + entries: nonEmptyEndpointNestedEntriesArray, + field: NonEmptyString, + type: t.keyof({ nested: null }), + }) +); +export type EndpointEntryNested = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts new file mode 100644 index 000000000000000..f68fea35e6fdfed --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { entriesArray, entriesArrayOrUndefined, entry } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryListMock } from '../entries_list/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; +import { getEntriesArrayMock } from './index.mock'; + +describe('Entries', () => { + describe('entry', () => { + test('it should validate a match entry', () => { + const payload = getEntryMatchMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a match_any entry', () => { + const payload = getEntryMatchAnyMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a exists entry', () => { + const payload = getEntryExistsMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a list entry', () => { + const payload = getEntryListMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation of nested entry', () => { + const payload = getEntryNestedMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "operator"', + 'Invalid value "nested" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + 'Invalid value "undefined" supplied to "list"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('entriesArray', () => { + test('it should validate an array with match entry', () => { + const payload = [getEntryMatchMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with match_any entry', () => { + const payload = [getEntryMatchAnyMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with exists entry', () => { + const payload = [getEntryExistsMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with list entry', () => { + const payload = [getEntryListMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with nested entry', () => { + const payload = [getEntryNestedMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with all types of entries', () => { + const payload = [...getEntriesArrayMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + describe('entriesArrayOrUndefined', () => { + test('it should validate undefined', () => { + const payload = undefined; + const decoded = entriesArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with nested entry', () => { + const payload = [getEntryNestedMock()]; + const decoded = entriesArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts new file mode 100644 index 000000000000000..ad2164a3862eb44 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryExists } from '.'; +import { EXISTS, FIELD, OPERATOR } from '../constants/index.mock'; + +export const getEntryExistsMock = (): EntryExists => ({ + field: FIELD, + operator: OPERATOR, + type: EXISTS, +}); + +export const getEntryExistsExcludedMock = (): EntryExists => ({ + ...getEntryExistsMock(), + operator: 'excluded', +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts new file mode 100644 index 000000000000000..05451b11de7a653 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryExistsMock } from './index.mock'; +import { entriesExists, EntryExists } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('entriesExists', () => { + test('it should validate an entry', () => { + const payload = getEntryExistsMock(); + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "included"', () => { + const payload = getEntryExistsMock(); + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryExistsMock(); + payload.operator = 'excluded'; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryExistsMock(), + field: '', + }; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryExists & { + extraKey?: string; + } = getEntryExistsMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryExistsMock()); + }); + + test('it should FAIL validation when "type" is not "exists"', () => { + const payload: Omit & { type: string } = { + ...getEntryExistsMock(), + type: 'match', + }; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts new file mode 100644 index 000000000000000..6d65d458583bd85 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { listOperator as operator } from '../list_operator'; + +export const entriesExists = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ exists: null }), + }) +); +export type EntryExists = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts new file mode 100644 index 000000000000000..2349b9d5ab2b34a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryList } from '.'; +import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../constants/index.mock'; + +export const getEntryListMock = (): EntryList => ({ + field: FIELD, + list: { id: LIST_ID, type: TYPE }, + operator: OPERATOR, + type: LIST, +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts new file mode 100644 index 000000000000000..5b7224277787570 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryListMock } from './index.mock'; +import { entriesList, EntryList } from '.'; + +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('entriesList', () => { + test('it should validate an entry', () => { + const payload = getEntryListMock(); + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryListMock(); + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryListMock(); + payload.operator = 'excluded'; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "list" is not expected value', () => { + const payload: Omit & { list: string } = { + ...getEntryListMock(), + list: 'someListId', + }; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "someListId" supplied to "list"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "list.id" is empty string', () => { + const payload: Omit & { list: { id: string; type: 'ip' } } = { + ...getEntryListMock(), + list: { id: '', type: 'ip' }, + }; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "list,id"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "lists"', () => { + const payload: Omit & { type: 'match_any' } = { + ...getEntryListMock(), + type: 'match_any', + }; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryList & { + extraKey?: string; + } = getEntryListMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryListMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts new file mode 100644 index 000000000000000..61d3c7b156fd275 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +import { type } from '../type'; +import { listOperator as operator } from '../list_operator'; + +export const entriesList = t.exact( + t.type({ + field: NonEmptyString, + list: t.exact(t.type({ id: NonEmptyString, type })), + operator, + type: t.keyof({ list: null }), + }) +); +export type EntryList = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts new file mode 100644 index 000000000000000..38c9f0f922c4678 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryMatch } from '.'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../constants/index.mock'; + +export const getEntryMatchMock = (): EntryMatch => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH, + value: ENTRY_VALUE, +}); + +export const getEntryMatchExcludeMock = (): EntryMatch => ({ + ...getEntryMatchMock(), + operator: 'excluded', +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts new file mode 100644 index 000000000000000..bff65ad7f6becf6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchMock } from './index.mock'; +import { entriesMatch, EntryMatch } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('entriesMatch', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchMock(); + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchMock(); + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchMock(), + field: '', + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchMock(), + value: ['some value'], + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchMock(), + value: '', + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchMock(), + type: 'match_any', + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatch & { + extraKey?: string; + } = getEntryMatchMock(); + payload.extraKey = 'some value'; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts new file mode 100644 index 000000000000000..4f04e01cf8f6301 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { listOperator as operator } from '../list_operator'; + +export const entriesMatch = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ match: null }), + value: NonEmptyString, + }) +); +export type EntryMatch = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts new file mode 100644 index 000000000000000..efaf23fe1e784d9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryMatchAny } from '.'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../constants/index.mock'; + +export const getEntryMatchAnyMock = (): EntryMatchAny => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH_ANY, + value: [ENTRY_VALUE], +}); + +export const getEntryMatchAnyExcludeMock = (): EntryMatchAny => ({ + ...getEntryMatchAnyMock(), + operator: 'excluded', + value: [ENTRY_VALUE, 'some other host name'], +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts new file mode 100644 index 000000000000000..c0eb017fdab5444 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchAnyMock } from './index.mock'; +import { entriesMatchAny, EntryMatchAny } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('entriesMatchAny', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchAnyMock(); + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchAnyMock(); + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "excluded"', () => { + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when field is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchAnyMock(), + field: '', + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is empty array', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchAnyMock(), + value: [], + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is not string array', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchAnyMock(), + value: 'some string', + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match_any"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchAnyMock(), + type: 'match', + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatchAny & { + extraKey?: string; + } = getEntryMatchAnyMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchAnyMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts new file mode 100644 index 000000000000000..86e97c579a02cd2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { NonEmptyString, nonEmptyOrNullableStringArray } from '@kbn/securitysolution-io-ts-types'; +import { listOperator as operator } from '../list_operator'; + +export const entriesMatchAny = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ match_any: null }), + value: nonEmptyOrNullableStringArray, + }) +); +export type EntryMatchAny = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts new file mode 100644 index 000000000000000..f81a8c6cba2eff2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryMatchWildcard } from '.'; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../constants/index.mock'; + +export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ + field: FIELD, + operator: OPERATOR, + type: WILDCARD, + value: ENTRY_VALUE, +}); + +export const getEntryMatchWildcardExcludeMock = (): EntryMatchWildcard => ({ + ...getEntryMatchWildcardMock(), + operator: 'excluded', +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts new file mode 100644 index 000000000000000..8a5a152ce7e6511 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchWildcardMock } from './index.mock'; +import { entriesMatchWildcard, EntryMatchWildcard } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('entriesMatchWildcard', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchWildcardMock(); + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchWildcardMock(); + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryMatchWildcardMock(); + payload.operator = 'excluded'; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchWildcardMock(), + field: '', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchWildcardMock(), + value: ['some value'], + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchWildcardMock(), + value: '', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "wildcard"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchWildcardMock(), + type: 'match', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatchWildcard & { + extraKey?: string; + } = getEntryMatchWildcardMock(); + payload.extraKey = 'some value'; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchWildcardMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts new file mode 100644 index 000000000000000..ea1953b983d4532 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { listOperator as operator } from '../list_operator'; + +export const entriesMatchWildcard = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ wildcard: null }), + value: NonEmptyString, + }) +); +export type EntryMatchWildcard = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts new file mode 100644 index 000000000000000..05f42cdf69bc05d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryNested } from '.'; +import { NESTED, NESTED_FIELD } from '../constants/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryMatchExcludeMock, getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from '../entry_match_any/index.mock'; + +export const getEntryNestedMock = (): EntryNested => ({ + entries: [getEntryMatchMock(), getEntryMatchAnyMock()], + field: NESTED_FIELD, + type: NESTED, +}); + +export const getEntryNestedExcludeMock = (): EntryNested => ({ + ...getEntryNestedMock(), + entries: [getEntryMatchExcludeMock(), getEntryMatchAnyExcludeMock()], +}); + +export const getEntryNestedMixedEntries = (): EntryNested => ({ + ...getEntryNestedMock(), + entries: [getEntryMatchMock(), getEntryMatchAnyExcludeMock(), getEntryExistsMock()], +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts new file mode 100644 index 000000000000000..b21737535fd77cc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryNestedMock } from './index.mock'; +import { entriesNested, EntryNested } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; + +describe('entriesNested', () => { + test('it should validate a nested entry', () => { + const payload = getEntryNestedMock(); + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "type" is not "nested"', () => { + const payload: Omit & { type: 'match' } = { + ...getEntryNestedMock(), + type: 'match', + }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { + field: string; + } = { ...getEntryNestedMock(), field: '' }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is not a string', () => { + const payload: Omit & { + field: number; + } = { ...getEntryNestedMock(), field: 1 }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "entries" is not a an array', () => { + const payload: Omit & { + entries: string; + } = { ...getEntryNestedMock(), entries: 'im a string' }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "im a string" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate when "entries" contains an entry item that is type "match"', () => { + const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host name'], + }, + ], + field: 'parent.field', + type: 'nested', + }); + }); + + test('it should validate when "entries" contains an entry item that is type "exists"', () => { + const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'exists', + }, + ], + field: 'parent.field', + type: 'nested', + }); + }); + + test('it should strip out extra keys', () => { + const payload: EntryNested & { + extraKey?: string; + } = getEntryNestedMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryNestedMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts new file mode 100644 index 000000000000000..f5ac68cc98702f9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { nonEmptyNestedEntriesArray } from '../non_empty_nested_entries_array'; + +export const entriesNested = t.exact( + t.type({ + entries: nonEmptyNestedEntriesArray, + field: NonEmptyString, + type: t.keyof({ nested: null }), + }) +); +export type EntryNested = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts new file mode 100644 index 000000000000000..5952bd2eda21f59 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +export const id = NonEmptyString; +export type Id = t.TypeOf; +export const idOrUndefined = t.union([id, t.undefined]); +export type IdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts new file mode 100644 index 000000000000000..3c60df315e43094 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './comment'; +export * from './constants'; +export * from './create_comment'; +export * from './created_at'; +export * from './created_by'; +export * from './default_comments_array'; +export * from './default_create_comments_array'; +export * from './default_namespace'; +export * from './default_namespace_array'; +export * from './default_update_comments_array'; +export * from './description'; +export * from './endpoint'; +export * from './entries'; +export * from './entries_exist'; +export * from './entries_list'; +export * from './entry_match'; +export * from './entry_match_any'; +export * from './entry_match_wildcard'; +export * from './entry_nested'; +export * from './exception_list'; +export * from './exception_list_item_type'; +export * from './id'; +export * from './item_id'; +export * from './list_operator'; +export * from './lists'; +export * from './lists_default_array'; +export * from './meta'; +export * from './name'; +export * from './non_empty_entries_array'; +export * from './non_empty_nested_entries_array'; +export * from './os_type'; +export * from './tags'; +export * from './type'; +export * from './update_comment'; +export * from './updated_at'; +export * from './updated_by'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts new file mode 100644 index 000000000000000..dcb03884eadab7f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +export const item_id = NonEmptyString; +export type ItemId = t.TypeOf; +export const itemIdOrUndefined = t.union([item_id, t.undefined]); +export type ItemIdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts new file mode 100644 index 000000000000000..396577d46cd72e6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const listOperator = t.keyof({ excluded: null, included: null }); +export type ListOperator = t.TypeOf; +export enum ListOperatorEnum { + INCLUDED = 'included', + EXCLUDED = 'excluded', +} + +export enum ListOperatorTypeEnum { + NESTED = 'nested', + MATCH = 'match', + MATCH_ANY = 'match_any', + WILDCARD = 'wildcard', + EXISTS = 'exists', + LIST = 'list', +} diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts new file mode 100644 index 000000000000000..e9f34c4cf789f2b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { List, ListArray } from '.'; +import { ENDPOINT_LIST_ID } from '../constants'; + +export const getListMock = (): List => ({ + id: 'some_uuid', + list_id: 'list_id_single', + namespace_type: 'single', + type: 'detection', +}); + +export const getEndpointListMock = (): List => ({ + id: ENDPOINT_LIST_ID, + list_id: ENDPOINT_LIST_ID, + namespace_type: 'agnostic', + type: 'endpoint', +}); + +export const getListArrayMock = (): ListArray => [getListMock(), getEndpointListMock()]; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts new file mode 100644 index 000000000000000..88dcc1ced8607d4 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointListMock, getListArrayMock, getListMock } from './index.mock'; +import { List, list, ListArray, listArray, ListArrayOrUndefined, listArrayOrUndefined } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('Lists', () => { + describe('list', () => { + test('it should validate a list', () => { + const payload = getListMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a list with "namespace_type" of "agnostic"', () => { + const payload = getEndpointListMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a list without an "id"', () => { + const payload = getListMock(); + // @ts-expect-error + delete payload.id; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a list without "namespace_type"', () => { + const payload = getListMock(); + // @ts-expect-error + delete payload.namespace_type; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: List & { + extraKey?: string; + } = getListMock(); + payload.extraKey = 'some value'; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getListMock()); + }); + }); + + describe('listArray', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when unexpected type found in array', () => { + const payload = ([1] as unknown) as ListArray; + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('listArrayOrUndefined', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an item that is not of type "list" in array', () => { + const payload = ([1] as unknown) as ListArrayOrUndefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts new file mode 100644 index 000000000000000..7881a6bb3322e11 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { exceptionListType } from '../exception_list'; +import { namespaceType } from '../default_namespace'; + +export const list = t.exact( + t.type({ + id: NonEmptyString, + list_id: NonEmptyString, + type: exceptionListType, + namespace_type: namespaceType, + }) +); + +export type List = t.TypeOf; +export const listArray = t.array(list); +export type ListArray = t.TypeOf; +export const listArrayOrUndefined = t.union([listArray, t.undefined]); +export type ListArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts new file mode 100644 index 000000000000000..58a52d26aa34f05 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultListArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getListArrayMock } from '../lists/index.mock'; + +describe('lists_default_array', () => { + test('it should return a default array when null', () => { + const payload = null; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should return a default array when undefined', () => { + const payload = undefined; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array of non accepted types', () => { + // Terrible casting for purpose of tests + const payload = [1] as unknown; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "DefaultListArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/name/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts new file mode 100644 index 000000000000000..98976f3cd6d21ce --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EntriesArray } from '../entries'; +import { nonEmptyEntriesArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { + getEntriesArrayMock, + getListAndNonListEntriesArrayMock, + getListEntriesArrayMock, +} from '../entries/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; + +describe('non_empty_entries_array', () => { + test('it should FAIL validation when given an empty array', () => { + const payload: EntriesArray = []; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload = undefined; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "null"', () => { + const payload = null; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of "match" entries', () => { + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "match_any" entries', () => { + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "exists" entries', () => { + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "list" entries', () => { + const payload: EntriesArray = [...getListEntriesArrayMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of entries', () => { + const payload: EntriesArray = [...getEntriesArrayMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { + const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Cannot have entry of type list and other']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given an array of non entries', () => { + const payload = [1]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts new file mode 100644 index 000000000000000..8ac958577f8d748 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EntriesArray } from '../entries'; +import { nonEmptyNestedEntriesArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; + +describe('non_empty_nested_entries_array', () => { + test('it should FAIL validation when given an empty array', () => { + const payload: EntriesArray = []; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload = undefined; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "null"', () => { + const payload = null; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of "match" entries', () => { + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "match_any" entries', () => { + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "exists" entries', () => { + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when given an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "operator"', + 'Invalid value "nested" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of entries', () => { + const payload: EntriesArray = [ + getEntryExistsMock(), + getEntryMatchAnyMock(), + getEntryMatchMock(), + ]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when given an array of non entries', () => { + const payload = [1]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts new file mode 100644 index 000000000000000..b7fa544c956eedd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { DefaultArray } from '@kbn/securitysolution-io-ts-types'; + +export const osType = t.keyof({ + linux: null, + macos: null, + windows: null, +}); +export type OsType = t.TypeOf; + +export const osTypeArray = DefaultArray(osType); +export type OsTypeArray = t.TypeOf; + +export const osTypeArrayOrUndefined = t.union([osTypeArray, t.undefined]); +export type OsTypeArrayOrUndefined = t.OutputOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts new file mode 100644 index 000000000000000..f0f23d9e4717de7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { DefaultStringArray } from '@kbn/securitysolution-io-ts-types'; + +export const tags = DefaultStringArray; +export type Tags = t.TypeOf; +export const tagsOrUndefined = t.union([tags, t.undefined]); +export type TagsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts new file mode 100644 index 000000000000000..50cacb8e0259b47 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +/** + * Types of all the regular single value list items but not exception list + * or exception list types. Those types are in the list_types folder. + */ +export const type = t.keyof({ + binary: null, + boolean: null, + byte: null, + date: null, + date_nanos: null, + date_range: null, + double: null, + double_range: null, + float: null, + float_range: null, + geo_point: null, + geo_shape: null, + half_float: null, + integer: null, + integer_range: null, + ip: null, + ip_range: null, + keyword: null, + long: null, + long_range: null, + shape: null, + short: null, + text: null, +}); + +export const typeOrUndefined = t.union([type, t.undefined]); +export type Type = t.TypeOf; +export type TypeOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts new file mode 100644 index 000000000000000..e9a56119dcc20e6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UpdateComment, UpdateCommentsArray } from '.'; +import { ID } from '../constants/index.mock'; + +export const getUpdateCommentMock = (): UpdateComment => ({ + comment: 'some comment', + id: ID, +}); + +export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [ + getUpdateCommentMock(), + getUpdateCommentMock(), +]; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts new file mode 100644 index 000000000000000..8dd0301c54dd849 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './index.mock'; +import { + UpdateComment, + updateComment, + UpdateCommentsArray, + updateCommentsArray, + UpdateCommentsArrayOrUndefined, + updateCommentsArrayOrUndefined, +} from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('CommentsUpdate', () => { + describe('updateComment', () => { + test('it should pass validation when supplied typical comment update', () => { + const payload = getUpdateCommentMock(); + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an undefined for "comment"', () => { + const payload = getUpdateCommentMock(); + // @ts-expect-error + delete payload.comment; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an empty string for "comment"', () => { + const payload = { ...getUpdateCommentMock(), comment: '' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); + expect(message.schema).toEqual({}); + }); + + test('it should pass validation when supplied an undefined for "id"', () => { + const payload = getUpdateCommentMock(); + delete payload.id; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an empty string for "id"', () => { + const payload = { ...getUpdateCommentMock(), id: '' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra key passed in', () => { + const payload: UpdateComment & { + extraKey?: string; + } = { ...getUpdateCommentMock(), extraKey: 'some new value' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getUpdateCommentMock()); + }); + }); + + describe('updateCommentsArray', () => { + test('it should pass validation when supplied an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArray; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('updateCommentsArrayOrUndefined', () => { + test('it should pass validation when supplied an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts new file mode 100644 index 000000000000000..5499690c97716f1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { id } from '../id'; + +export const updateComment = t.intersection([ + t.exact( + t.type({ + comment: NonEmptyString, + }) + ), + t.exact( + t.partial({ + id, + }) + ), +]); + +export type UpdateComment = t.TypeOf; +export const updateCommentsArray = t.array(updateComment); +export type UpdateCommentsArray = t.TypeOf; +export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); +export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json new file mode 100644 index 000000000000000..d926653a4230be1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-list-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel new file mode 100644 index 000000000000000..0a21f5ed94f0100 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -0,0 +1,93 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-types/README.md b/packages/kbn-securitysolution-io-ts-types/README.md new file mode 100644 index 000000000000000..552c663d819e3dc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-types + +Generic io-ts types that are not specific to any particular domain for use with other packages or across different plugins/domains + +Related packages are: +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-alerting-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-types/jest.config.js b/packages/kbn-securitysolution-io-ts-types/jest.config.js new file mode 100644 index 000000000000000..18d31eaa75219ec --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-types/package.json b/packages/kbn-securitysolution-io-ts-types/package.json new file mode 100644 index 000000000000000..0381a6d24a136e8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts new file mode 100644 index 000000000000000..4ca45e7de3377ef --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +const testSchema = t.keyof({ + valid: true, + also_valid: true, +}); +type TestSchema = t.TypeOf; + +const defaultArraySchema = DefaultArray(testSchema); + +describe('default_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of testSchema', () => { + const payload: TestSchema[] = ['valid']; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of valid testSchema strings', () => { + const payload = ['valid', 'also_valid']; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['valid', 123]; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "123" supplied to "DefaultArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an array with an invalid string', () => { + const payload = ['valid', 'invalid']; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "DefaultArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts new file mode 100644 index 000000000000000..c87a67ec4e5d4be --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultBooleanFalse } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_boolean_false', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts new file mode 100644 index 000000000000000..3ec33fda392e4b8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultBooleanTrue } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_boolean_true', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultBooleanTrue"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default true', () => { + const payload = null; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts new file mode 100644 index 000000000000000..02fb74510d604ba --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultEmptyString } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_empty_string', () => { + test('it should validate a regular string', () => { + const payload = 'some string'; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of ""', () => { + const payload = null; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(''); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts new file mode 100644 index 000000000000000..7b1f217f55ad561 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultStringArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_string_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of strings', () => { + const payload = ['value 1', 'value 2']; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['value 1', 5]; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts new file mode 100644 index 000000000000000..3e96c942de74ae3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultStringBooleanFalse } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_string_boolean_false', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultStringBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); + + test('it should return a default false when given a string of "false"', () => { + const payload = 'false'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); + + test('it should return a default true when given a string of "true"', () => { + const payload = 'true'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); + + test('it should return a default true when given a string of "TruE"', () => { + const payload = 'TruE'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); + + test('it should not work with a string of junk "junk"', () => { + const payload = 'junk'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "junk" supplied to "DefaultStringBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not work with an empty string', () => { + const payload = ''; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "DefaultStringBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts new file mode 100644 index 000000000000000..c471141a99a7636 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultUuid } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.test.ts new file mode 100644 index 000000000000000..fd7b12123b6bb16 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultVersionNumber } from '../default_version_number'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_version_number', () => { + test('it should validate a version number', () => { + const payload = 5; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a 0', () => { + const payload = 0; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a -1', () => { + const payload = -1; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 1', () => { + const payload = null; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts new file mode 100644 index 000000000000000..5b7863947cad43c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EmptyStringArray, EmptyStringArrayEncoded } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('empty_string_array', () => { + test('it should validate "null" and create an empty array', () => { + const payload: EmptyStringArrayEncoded = null; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate "undefined" and create an empty array', () => { + const payload: EmptyStringArrayEncoded = undefined; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { + const payload: EmptyStringArrayEncoded = 'a'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a']); + }); + + test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { + const payload: EmptyStringArrayEncoded = 'a,b'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b']); + }); + + test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { + const payload: EmptyStringArrayEncoded = 'a,b,c'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); + + test('it should FAIL validation of number', () => { + const payload: number = 5; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "EmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { + const payload: EmptyStringArrayEncoded = ' a, b, c '; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts new file mode 100644 index 000000000000000..fc0f017016e9f6d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './default_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_uuid'; +export * from './default_version_number'; +export * from './empty_string_array'; +export * from './iso_date_string'; +export * from './non_empty_array'; +export * from './non_empty_or_nullable_string_array'; +export * from './non_empty_string'; +export * from './non_empty_string_array'; +export * from './operator'; +export * from './only_false_allowed'; +export * from './parse_schedule_dates'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './string_to_positive_number'; +export * from './uuid'; +export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts new file mode 100644 index 000000000000000..e70a738d7336e3a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { IsoDateString } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('ios_date_string', () => { + test('it should validate a iso string', () => { + const payload = '2020-02-26T00:32:34.541Z'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an epoch number', () => { + const payload = '1582677283067'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1582677283067" supplied to "IsoDateString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a number such as 2000', () => { + const payload = '2000'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "2000" supplied to "IsoDateString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a UTC', () => { + const payload = 'Wed, 26 Feb 2020 00:36:20 GMT'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "Wed, 26 Feb 2020 00:36:20 GMT" supplied to "IsoDateString"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts new file mode 100644 index 000000000000000..05861953601422a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { NonEmptyArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +const testSchema = t.keyof({ + valid: true, + also_valid: true, +}); +type TestSchema = t.TypeOf; + +const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray'); + +describe('non empty array', () => { + test('it should generate the correct name for non empty array', () => { + const newTestSchema = NonEmptyArray(testSchema); + expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">'); + }); + + test('it should use a supplied name override', () => { + const newTestSchema = NonEmptyArray(testSchema, 'someName'); + expect(newTestSchema.name).toEqual('someName'); + }); + + test('it should NOT validate an empty array', () => { + const payload: string[] = []; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of testSchema', () => { + const payload: TestSchema[] = ['valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of valid testSchema strings', () => { + const payload: TestSchema[] = ['valid', 'also_valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['valid', 123]; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "123" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an array with an invalid string', () => { + const payload = ['valid', 'invalid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a null value', () => { + const payload = null; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts new file mode 100644 index 000000000000000..355bd9d20061e7c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { nonEmptyOrNullableStringArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('nonEmptyOrNullableStringArray', () => { + test('it should FAIL validation when given an empty array', () => { + const payload: string[] = []; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload = undefined; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "null"', () => { + const payload = null; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given an array of with an empty string', () => { + const payload: string[] = ['im good', '']; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["im good",""]" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given an array of non strings', () => { + const payload = [1]; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[1]" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts new file mode 100644 index 000000000000000..ae3b8cd9acad529 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { NonEmptyString } from '.'; + +describe('non_empty_string', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "NonEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "NonEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate empty spaces', () => { + const payload = ' '; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value " " supplied to "NonEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts new file mode 100644 index 000000000000000..f56fa7faed2a4ea --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { NonEmptyStringArray } from '.'; + +describe('non_empty_string_array', () => { + test('it should FAIL validation when given "null"', () => { + const payload: NonEmptyStringArray | null = null; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload: NonEmptyStringArray | undefined = undefined; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation of single value of an empty string ""', () => { + const payload: NonEmptyStringArray = ''; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { + const payload: NonEmptyStringArray = 'a'; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a']); + }); + + test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { + const payload: NonEmptyStringArray = 'a,b'; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b']); + }); + + test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { + const payload: NonEmptyStringArray = 'a,b,c'; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); + + test('it should FAIL validation of number', () => { + const payload: number = 5; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "NonEmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { + const payload: NonEmptyStringArray = ' a, b, c '; + const decoded = NonEmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts new file mode 100644 index 000000000000000..7eead15f693512e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the NonEmptyStringArray as: + * - A string that is not empty (which will be turned into an array of size 1) + * - A comma separated string that can turn into an array by splitting on it + * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] + */ +export const NonEmptyStringArray = new t.Type( + 'NonEmptyStringArray', + t.array(t.string).is, + (input, context): Either => { + if (typeof input === 'string' && input.trim() !== '') { + const arrayValues = input + .trim() + .split(',') + .map((value) => value.trim()); + const emptyValueFound = arrayValues.some((value) => value === ''); + if (emptyValueFound) { + return t.failure(input, context); + } else { + return t.success(arrayValues); + } + } else { + return t.failure(input, context); + } + }, + String +); + +export type NonEmptyStringArray = t.OutputOf; + +export type NonEmptyStringArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts new file mode 100644 index 000000000000000..de05872c0dc31f1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { OnlyFalseAllowed } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('only_false_allowed', () => { + test('it should validate a boolean false as false', () => { + const payload = false; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a boolean true', () => { + const payload = true; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "true" supplied to "DefaultBooleanTrue"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultBooleanTrue"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts b/packages/kbn-securitysolution-io-ts-types/src/operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts new file mode 100644 index 000000000000000..d6a99b5fbf88088 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import moment from 'moment'; +import dateMath from '@elastic/datemath'; + +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate != null ? formattedDate : null; +}; diff --git a/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts new file mode 100644 index 000000000000000..deea8951a3d39be --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { PositiveInteger } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('positive_integer_greater_than_zero', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a zero', () => { + const payload = 0; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveInteger"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "PositiveInteger"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts new file mode 100644 index 000000000000000..4ea6fe920cf140a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { PositiveIntegerGreaterThanZero } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('positive_integer_greater_than_zero', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a zero', () => { + const payload = 0; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts b/packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts new file mode 100644 index 000000000000000..4333fab102d44b7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { UUID } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('uuid', () => { + test('it should validate a uuid', () => { + const payload = '4656dc92-5832-11ea-8e2d-0242ac130003'; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a non uuid', () => { + const payload = '4656dc92-5832-11ea-8e2d'; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "4656dc92-5832-11ea-8e2d" supplied to "UUID"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "UUID"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/version/index.ts b/packages/kbn-securitysolution-io-ts-types/src/version/index.ts new file mode 100644 index 000000000000000..245b64781a7f8c1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/version/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; + +/** + * Note this is just a positive number, but we use it as a type here which is still ok. + * This type was originally from "x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts" + * but is moved here to make things more portable. No unit tests, but see PositiveIntegerGreaterThanZero integer for unit tests. + */ +export const version = PositiveIntegerGreaterThanZero; +export type Version = t.TypeOf; + +export const versionOrUndefined = t.union([version, t.undefined]); +export type VersionOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json new file mode 100644 index 000000000000000..42a059439ecb5be --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-utils/README.md b/packages/kbn-securitysolution-io-ts-utils/README.md index 908651b50b80a3b..146f965391aa086 100644 --- a/packages/kbn-securitysolution-io-ts-utils/README.md +++ b/packages/kbn-securitysolution-io-ts-utils/README.md @@ -1,10 +1,12 @@ # kbn-securitysolution-io-ts-utils -Temporary location for all the io-ts-utils from security solutions. This is a lift-and-shift, where -we are moving them here for phase 1. +Very small set of utilities for io-ts which we use across plugins within security solutions such as securitysolution, lists, cases, etc... +This folder should remain small and concise since it is pulled into front end and the more files we add the more weight will be added to all +of the plugins. Also, any new dependencies added to this will add weight here and the other plugins, so be careful of what is added here. -Phase 2 is deprecating across plugins any copied code or sharing of io-ts utils that are now in here. +You might consider making another package instead and putting a dependency on this one if needed, instead. -Phase 3 is replacing those deprecated types with the ones in here. - -Phase 4+ is (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project \ No newline at end of file +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts deleted file mode 100644 index f77903d2d030dab..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultVersionNumber } from '../default_version_number'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_version_number', () => { - test('it should validate a version number', () => { - const payload = 5; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a 0', () => { - const payload = 0; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a -1', () => { - const payload = -1; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 1', () => { - const payload = null; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts deleted file mode 100644 index 82fa884b1c577dc..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -const testSchema = t.keyof({ - valid: true, - also_valid: true, -}); -type TestSchema = t.TypeOf; - -const defaultArraySchema = DefaultArray(testSchema); - -describe('default_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of testSchema', () => { - const payload: TestSchema[] = ['valid']; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of valid testSchema strings', () => { - const payload = ['valid', 'also_valid']; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['valid', 123]; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "123" supplied to "DefaultArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an array with an invalid string', () => { - const payload = ['valid', 'invalid']; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid" supplied to "DefaultArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts deleted file mode 100644 index bddf9cc0747eabc..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_boolean_false', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts deleted file mode 100644 index a05fb586c2e9254..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultBooleanTrue } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_boolean_true', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultBooleanTrue"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default true', () => { - const payload = null; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts deleted file mode 100644 index 5bdc9b298649e59..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultEmptyString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_empty_string', () => { - test('it should validate a regular string', () => { - const payload = 'some string'; - const decoded = DefaultEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of ""', () => { - const payload = null; - const decoded = DefaultEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(''); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts deleted file mode 100644 index 1f81f056386d7f7..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultExportFileName } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_export_file_name', () => { - test('it should validate a regular string', () => { - const payload = 'some string'; - const decoded = DefaultExportFileName.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultExportFileName.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultExportFileName"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "export.ndjson"', () => { - const payload = null; - const decoded = DefaultExportFileName.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('export.ndjson'); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts deleted file mode 100644 index c1261f514540bd8..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultFromString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_from_string', () => { - test('it should validate a from string', () => { - const payload = 'now-20m'; - const decoded = DefaultFromString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultFromString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultFromString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "now-6m"', () => { - const payload = null; - const decoded = DefaultFromString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('now-6m'); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts deleted file mode 100644 index c4a0dc3664d0e0d..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultIntervalString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_interval_string', () => { - test('it should validate a interval string', () => { - const payload = '20m'; - const decoded = DefaultIntervalString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultIntervalString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultIntervalString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "5m"', () => { - const payload = null; - const decoded = DefaultIntervalString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('5m'); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts deleted file mode 100644 index 072c541a808a351..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { Language } from '../language'; -import { DefaultLanguageString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_language_string', () => { - test('it should validate a string', () => { - const payload: Language = 'lucene'; - const decoded = DefaultLanguageString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultLanguageString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultLanguageString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "kuery"', () => { - const payload = null; - const decoded = DefaultLanguageString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('kuery'); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts deleted file mode 100644 index bf703fa52d844ac..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultMaxSignalsNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { DEFAULT_MAX_SIGNALS } from '../constants'; - -describe('default_from_string', () => { - test('it should validate a max signal number', () => { - const payload = 5; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultMaxSignals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a zero', () => { - const payload = 0; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultMaxSignals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a negative number', () => { - const payload = -1; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultMaxSignals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of DEFAULT_MAX_SIGNALS', () => { - const payload = null; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts deleted file mode 100644 index 3bcad15a7ebb8b7..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_page', () => { - test('it should validate a regular number greater than zero', () => { - const payload = 5; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a string of a number', () => { - const payload = '5'; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(5); - }); - - test('it should not validate a junk string', () => { - const payload = 'invalid-string'; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a zero', () => { - const payload = 0; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a negative number', () => { - const payload = -1; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 20', () => { - const payload = null; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts deleted file mode 100644 index 056005b452a033d..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; - -/** - * Types the DefaultPerPage as: - * - If a string this will convert the string to a number - * - If null or undefined, then a default of 1 will be used - * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero - */ -export const DefaultPage = new t.Type( - 'DefaultPerPage', - t.number.is, - (input, context): Either => { - if (input == null) { - return t.success(1); - } else if (typeof input === 'string') { - return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); - } else { - return PositiveIntegerGreaterThanZero.validate(input, context); - } - }, - t.identity -); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts deleted file mode 100644 index f7361ba12a570d0..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultPerPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_per_page', () => { - test('it should validate a regular number greater than zero', () => { - const payload = 5; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a string of a number', () => { - const payload = '5'; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(5); - }); - - test('it should not validate a junk string', () => { - const payload = 'invalid-string'; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a zero', () => { - const payload = 0; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a negative number', () => { - const payload = -1; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 20', () => { - const payload = null; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(20); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts deleted file mode 100644 index 026642f91c08a09..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; - -/** - * Types the DefaultPerPage as: - * - If a string this will convert the string to a number - * - If null or undefined, then a default of 20 will be used - * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero - */ -export const DefaultPerPage = new t.Type( - 'DefaultPerPage', - t.number.is, - (input, context): Either => { - if (input == null) { - return t.success(20); - } else if (typeof input === 'string') { - return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); - } else { - return PositiveIntegerGreaterThanZero.validate(input, context); - } - }, - t.identity -); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts deleted file mode 100644 index c7137d9c56b0d1f..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_string_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of strings', () => { - const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts deleted file mode 100644 index 2443e8f71fecdb9..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultStringBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_string_boolean_false', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); - - test('it should return a default false when given a string of "false"', () => { - const payload = 'false'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); - - test('it should return a default true when given a string of "true"', () => { - const payload = 'true'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); - - test('it should return a default true when given a string of "TruE"', () => { - const payload = 'TruE'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); - - test('it should not work with a string of junk "junk"', () => { - const payload = 'junk'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "junk" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not work with an empty string', () => { - const payload = ''; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts deleted file mode 100644 index ac86b5508ff1424..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { Threats } from '../threat'; -import { DefaultThreatArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_threat_null', () => { - test('it should validate an empty array', () => { - const payload: Threats = []; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of threats', () => { - const payload: Threats = [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - ]; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - 5, - ]; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultThreatArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default empty array if not provided a value', () => { - const payload = null; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts deleted file mode 100644 index 4b8877bd532c289..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { Throttle } from '../throttle'; -import { DefaultThrottleNull } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_throttle_null', () => { - test('it should validate a throttle string', () => { - const payload: Throttle = 'some string'; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = 5; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultThreatNull"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default "null" if not provided a value', () => { - const payload = undefined; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(null); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts deleted file mode 100644 index bcab8ebd5f17c56..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultToString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_to_string', () => { - test('it should validate a to string', () => { - const payload = 'now-5m'; - const decoded = DefaultToString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultToString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultToString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "now"', () => { - const payload = null; - const decoded = DefaultToString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('now'); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts deleted file mode 100644 index d8cdff416037c40..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultUuid } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_uuid', () => { - test('it should validate a regular string', () => { - const payload = '1'; - const decoded = DefaultUuid.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultUuid.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of a uuid', () => { - const payload = null; - const decoded = DefaultUuid.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts deleted file mode 100644 index b9e9a3ff367e45b..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultVersionNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_version_number', () => { - test('it should validate a version number', () => { - const payload = 5; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a 0', () => { - const payload = 0; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a -1', () => { - const payload = -1; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 1', () => { - const payload = null; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts deleted file mode 100644 index 245ff9d0db7ddb6..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { version, Version } from '../version'; - -/** - * Types the DefaultVersionNumber as: - * - If null or undefined, then a default of the number 1 will be used - */ -export const DefaultVersionNumber = new t.Type( - 'DefaultVersionNumber', - version.is, - (input, context): Either => - input == null ? t.success(1) : version.validate(input, context), - t.identity -); - -export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts deleted file mode 100644 index 86ffba6eeb60aa5..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { EmptyStringArray, EmptyStringArrayEncoded } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('empty_string_array', () => { - test('it should validate "null" and create an empty array', () => { - const payload: EmptyStringArrayEncoded = null; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should validate "undefined" and create an empty array', () => { - const payload: EmptyStringArrayEncoded = undefined; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { - const payload: EmptyStringArrayEncoded = 'a'; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a']); - }); - - test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { - const payload: EmptyStringArrayEncoded = 'a,b'; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a', 'b']); - }); - - test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { - const payload: EmptyStringArrayEncoded = 'a,b,c'; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a', 'b', 'c']); - }); - - test('it should FAIL validation of number', () => { - const payload: number = 5; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "EmptyStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { - const payload: EmptyStringArrayEncoded = ' a, b, c '; - const decoded = EmptyStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['a', 'b', 'c']); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts index 7df66dcd13596be..ec37adf1221f2ac 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts @@ -21,7 +21,9 @@ export const formatErrors = (errors: t.Errors): string[] => { .map((entry) => entry.key) .join(','); - const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); + const nameContext = error.context.find( + (entry) => entry.type != null && entry.type.name != null && entry.type.name.length > 0 + ); const suppliedValue = keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts deleted file mode 100644 index 963e2fa0444f07a..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Either } from 'fp-ts/lib/Either'; -import * as t from 'io-ts'; -import { parseScheduleDates } from '../parse_schedule_dates'; - -const stringValidator = (input: unknown): input is string => typeof input === 'string'; - -export const from = new t.Type( - 'From', - t.string.is, - (input, context): Either => { - if (stringValidator(input) && parseScheduleDates(input) == null) { - return t.failure(input, context, 'Failed to parse "from" on rule param'); - } - return t.string.validate(input, context); - }, - t.identity -); -export type From = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts deleted file mode 100644 index 7b187d7730f739f..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../non_empty_string'; - -export const id = NonEmptyString; -export type Id = t.TypeOf; -export const idOrUndefined = t.union([id, t.undefined]); -export type IdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts index bae90fed29deac4..c21096e497134b1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -7,68 +7,7 @@ */ export * from './format_errors'; -export * from './actions'; -export * from './constants'; -export * from './created_at'; -export * from './created_by'; -export * from './default_version_number'; -export * from './default_actions_array'; -export * from './default_array'; -export * from './default_boolean_false'; -export * from './default_boolean_true'; -export * from './default_empty_string'; -export * from './default_export_file_name'; -export * from './default_from_string'; -export * from './default_interval_string'; -export * from './default_language_string'; -export * from './default_max_signals_number'; -export * from './default_page'; -export * from './default_per_page'; -export * from './default_risk_score_mapping_array'; -export * from './default_severity_mapping_array'; -export * from './default_string_array'; -export * from './default_string_boolean_false'; -export * from './default_threat_array'; -export * from './default_throttle_null'; -export * from './default_to_string'; -export * from './default_uuid'; -export * from './default_version_number'; -export * from './description'; -export * from './empty_string_array'; export * from './exact_check'; export * from './format_errors'; -export * from './from'; -export * from './id'; -export * from './iso_date_string'; -export * from './language'; -export * from './max_signals'; -export * from './meta'; -export * from './name'; -export * from './non_empty_array'; -export * from './non_empty_or_nullable_string_array'; -export * from './non_empty_string'; -export * from './normalized_ml_job_id'; -export * from './only_false_allowed'; -export * from './operator'; -export * from './parse_schedule_dates'; -export * from './positive_integer'; -export * from './positive_integer_greater_than_zero'; -export * from './references_default_array'; -export * from './risk_score'; -export * from './risk_score_mapping'; -export * from './saved_object_attributes'; -export * from './severity'; -export * from './severity_mapping'; -export * from './string_to_positive_number'; -export * from './tags'; -export * from './threat'; -export * from './threat_mapping'; -export * from './threat_subtechnique'; -export * from './threat_tactic'; -export * from './threat_technique'; -export * from './throttle'; -export * from './updated_at'; -export * from './updated_by'; -export * from './uuid'; +export * from './test_utils'; export * from './validate'; -export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts deleted file mode 100644 index 4b73ed1b136dcbc..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { IsoDateString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('ios_date_string', () => { - test('it should validate a iso string', () => { - const payload = '2020-02-26T00:32:34.541Z'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an epoch number', () => { - const payload = '1582677283067'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1582677283067" supplied to "IsoDateString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a number such as 2000', () => { - const payload = '2000'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "2000" supplied to "IsoDateString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a UTC', () => { - const payload = 'Wed, 26 Feb 2020 00:36:20 GMT'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "Wed, 26 Feb 2020 00:36:20 GMT" supplied to "IsoDateString"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts deleted file mode 100644 index fc3f70f1f2d8893..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -export const language = t.keyof({ eql: null, kuery: null, lucene: null }); -export type Language = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts deleted file mode 100644 index 56440d628e4aa44..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Comment, CommentsArray } from '.'; -import { DATE_NOW, ID, USER } from '../../constants/index.mock'; - -export const getCommentsMock = (): Comment => ({ - comment: 'some old comment', - created_at: DATE_NOW, - created_by: USER, - id: ID, -}); - -export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts deleted file mode 100644 index 0f0bfac5e20684a..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getCommentsArrayMock, getCommentsMock } from './index.mock'; -import { - Comment, - comment, - CommentsArray, - commentsArray, - CommentsArrayOrUndefined, - commentsArrayOrUndefined, -} from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { DATE_NOW } from '../../constants/index.mock'; - -describe('Comment', () => { - describe('comment', () => { - test('it fails validation when "id" is undefined', () => { - const payload = { ...getCommentsMock(), id: undefined }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it passes validation with a typical comment', () => { - const payload = getCommentsMock(); - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation with "updated_at" and "updated_by" fields included', () => { - const payload = getCommentsMock(); - payload.updated_at = DATE_NOW; - payload.updated_by = 'someone'; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "comment" is an empty string', () => { - const payload: Omit & { comment: string } = { - ...getCommentsMock(), - comment: '', - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { - ...getCommentsMock(), - comment: ['some value'], - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "comment"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "created_at" is not a string', () => { - const payload: Omit & { created_at: number } = { - ...getCommentsMock(), - created_at: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "created_at"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "created_by" is not a string', () => { - const payload: Omit & { created_by: number } = { - ...getCommentsMock(), - created_by: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "created_by"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "updated_at" is not a string', () => { - const payload: Omit & { updated_at: number } = { - ...getCommentsMock(), - updated_at: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "updated_at"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "updated_by" is not a string', () => { - const payload: Omit & { updated_by: number } = { - ...getCommentsMock(), - updated_by: 1, - }; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "updated_by"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: Comment & { - extraKey?: string; - } = getCommentsMock(); - payload.extraKey = 'some value'; - const decoded = comment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getCommentsMock()); - }); - }); - - describe('commentsArray', () => { - test('it passes validation an array of Comment', () => { - const payload = getCommentsArrayMock(); - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation when a Comment includes "updated_at" and "updated_by"', () => { - const commentsPayload = getCommentsMock(); - commentsPayload.updated_at = DATE_NOW; - commentsPayload.updated_by = 'someone'; - const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()]; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when array includes non Comment types', () => { - const payload = ([1] as unknown) as CommentsArray; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('commentsArrayOrUndefined', () => { - test('it passes validation an array of Comment', () => { - const payload = getCommentsArrayMock(); - const decoded = commentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation when undefined', () => { - const payload = undefined; - const decoded = commentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when array includes non Comment types', () => { - const payload = ([1] as unknown) as CommentsArrayOrUndefined; - const decoded = commentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts deleted file mode 100644 index 783d8606b8a9664..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -import { NonEmptyString } from '../../non_empty_string'; -import { created_at } from '../../created_at'; -import { created_by } from '../../created_by'; -import { id } from '../../id'; -import { updated_at } from '../../updated_at'; -import { updated_by } from '../../updated_by'; - -export const comment = t.intersection([ - t.exact( - t.type({ - comment: NonEmptyString, - created_at, - created_by, - id, - }) - ), - t.exact( - t.partial({ - updated_at, - updated_by, - }) - ), -]); - -export const commentsArray = t.array(comment); -export type CommentsArray = t.TypeOf; -export type Comment = t.TypeOf; -export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); -export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts deleted file mode 100644 index 1ac605e232ea1e0..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getCreateCommentsArrayMock, getCreateCommentsMock } from './index.mock'; -import { - CreateComment, - createComment, - CreateCommentsArray, - createCommentsArray, - CreateCommentsArrayOrUndefined, - createCommentsArrayOrUndefined, -} from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('CreateComment', () => { - describe('createComment', () => { - test('it passes validation with a default comment', () => { - const payload = getCreateCommentsMock(); - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "{| comment: NonEmptyString |}"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when "comment" is not a string', () => { - const payload: Omit & { comment: string[] } = { - ...getCreateCommentsMock(), - comment: ['some value'], - }; - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "comment"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: CreateComment & { - extraKey?: string; - } = getCreateCommentsMock(); - payload.extraKey = 'some value'; - const decoded = createComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getCreateCommentsMock()); - }); - }); - - describe('createCommentsArray', () => { - test('it passes validation an array of comments', () => { - const payload = getCreateCommentsArrayMock(); - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when undefined', () => { - const payload = undefined; - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it fails validation when array includes non comments types', () => { - const payload = ([1] as unknown) as CreateCommentsArray; - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('createCommentsArrayOrUndefined', () => { - test('it passes validation an array of comments', () => { - const payload = getCreateCommentsArrayMock(); - const decoded = createCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it passes validation when undefined', () => { - const payload = undefined; - const decoded = createCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it fails validation when array includes non comments types', () => { - const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined; - const decoded = createCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts deleted file mode 100644 index 438f946e796d6a2..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; - -export const createComment = t.exact( - t.type({ - comment: NonEmptyString, - }) -); - -export type CreateComment = t.TypeOf; -export const createCommentsArray = t.array(createComment); -export type CreateCommentsArray = t.TypeOf; -export type CreateComments = t.TypeOf; -export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); -export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts deleted file mode 100644 index 5e667380e2adfa0..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { CommentsArray } from '../comment'; -import { DefaultCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getCommentsArrayMock } from '../comment/index.mock'; - -describe('default_comments_array', () => { - test('it should pass validation when supplied an empty array', () => { - const payload: CommentsArray = []; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied an array of comments', () => { - const payload: CommentsArray = getCommentsArrayMock(); - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an array of numbers', () => { - const payload = [1]; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an array of strings', () => { - const payload = ['some string']; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts deleted file mode 100644 index a4581fabbf6a929..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { CommentsArray } from '../comment'; -import { DefaultCommentsArray } from '../default_comments_array'; -import { getCommentsArrayMock } from '../comment/index.mock'; - -describe('default_comments_array', () => { - test('it should pass validation when supplied an empty array', () => { - const payload: CommentsArray = []; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied an array of comments', () => { - const payload: CommentsArray = getCommentsArrayMock(); - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an array of numbers', () => { - const payload = [1]; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an array of strings', () => { - const payload = ['some string']; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts deleted file mode 100644 index 1decca0de6c50fa..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultNamespace } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('default_namespace', () => { - test('it should validate "single"', () => { - const payload = 'single'; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate "agnostic"', () => { - const payload = 'agnostic'; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it defaults to "single" if "undefined"', () => { - const payload = undefined; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('single'); - }); - - test('it defaults to "single" if "null"', () => { - const payload = null; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('single'); - }); - - test('it should FAIL validation if not "single" or "agnostic"', () => { - const payload = 'something else'; - const decoded = DefaultNamespace.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - `Invalid value "something else" supplied to "DefaultNamespace"`, - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts deleted file mode 100644 index 8bc7a16b960971b..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('default_namespace_array', () => { - test('it should validate "null" single item as an array with a "single" value', () => { - const payload: DefaultNamespaceArrayType = null; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single']); - }); - - test('it should FAIL validation of numeric value', () => { - const payload = 5; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultNamespaceArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate "undefined" item as an array with a "single" value', () => { - const payload: DefaultNamespaceArrayType = undefined; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single']); - }); - - test('it should validate "single" as an array of a "single" value', () => { - const payload: DefaultNamespaceArrayType = 'single'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([payload]); - }); - - test('it should validate "agnostic" as an array of a "agnostic" value', () => { - const payload: DefaultNamespaceArrayType = 'agnostic'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([payload]); - }); - - test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => { - const payload: DefaultNamespaceArrayType = 'agnostic,single'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['agnostic', 'single']); - }); - - test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => { - const payload: DefaultNamespaceArrayType = 'single,agnostic,single'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single', 'agnostic', 'single']); - }); - - test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => { - const payload: DefaultNamespaceArrayType = ' single, agnostic, single '; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(['single', 'agnostic', 'single']); - }); - - test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { - const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; - const decoded = DefaultNamespaceArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "junk" supplied to "DefaultNamespaceArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts deleted file mode 100644 index f52baa49530ecb8..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { UpdateCommentsArray } from '../update_comment'; -import { DefaultUpdateCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getUpdateCommentsArrayMock } from '../update_comment/index.mock'; - -describe('default_update_comments_array', () => { - test('it should pass validation when supplied an empty array', () => { - const payload: UpdateCommentsArray = []; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied an array of comments', () => { - const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an array of numbers', () => { - const payload = [1]; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an array of strings', () => { - const payload = ['some string']; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultUpdateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts deleted file mode 100644 index f5cb89ee7960718..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; -import { - endpointEntriesArray, - nonEmptyEndpointEntriesArray, - NonEmptyEndpointEntriesArray, -} from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; -import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; -import { getEndpointEntriesArrayMock } from './index.mock'; -import { getEntryListMock } from '../../entries_list/index.mock'; -import { getEntryExistsMock } from '../../entries_exist/index.mock'; - -describe('Endpoint', () => { - describe('entriesArray', () => { - test('it should validate an array with match entry', () => { - const payload = [getEndpointEntryMatchMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with match_any entry', () => { - const payload = [getEndpointEntryMatchAnyMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate an empty array', () => { - const payload: NonEmptyEndpointEntriesArray = []; - const decoded = nonEmptyEndpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyEndpointEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { - const payload: NonEmptyEndpointEntriesArray = [getEndpointEntryMatchAnyMock()]; - const guarded = nonEmptyEndpointEntriesArray.is(payload); - expect(guarded).toBeTruthy(); - }); - - test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { - const payload: NonEmptyEndpointEntriesArray = []; - const guarded = nonEmptyEndpointEntriesArray.is(payload); - expect(guarded).toBeFalsy(); - }); - - test('it should NOT validate an array with exists entry', () => { - const payload = [getEntryExistsMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "exists" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate an array with list entry', () => { - const payload = [getEntryListMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "list" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array with nested entry', () => { - const payload = [getEndpointEntryNestedMock()]; - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with all types of entries', () => { - const payload = getEndpointEntriesArrayMock(); - const decoded = endpointEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts deleted file mode 100644 index 7104406c4869ca0..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EndpointEntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants/index.mock'; - -export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ - field: FIELD, - operator: OPERATOR, - type: MATCH, - value: ENTRY_VALUE, -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts deleted file mode 100644 index cc0423fc119c796..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEndpointEntryMatchMock } from './index.mock'; -import { EndpointEntryMatch, endpointEntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { getEntryMatchMock } from '../../entry_match/index.mock'; - -describe('endpointEntryMatch', () => { - test('it should validate an entry', () => { - const payload = getEndpointEntryMatchMock(); - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate when "operator" is "excluded"', () => { - // Use the generic entry mock so we can test operator: excluded - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "excluded" supplied to "operator"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEndpointEntryMatchMock(), - field: '', - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is not string', () => { - const payload: Omit & { value: string[] } = { - ...getEndpointEntryMatchMock(), - value: ['some value'], - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is empty string', () => { - const payload: Omit & { value: string } = { - ...getEndpointEntryMatchMock(), - value: '', - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match"', () => { - const payload: Omit & { type: string } = { - ...getEndpointEntryMatchMock(), - type: 'match_any', - }; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "match_any" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EndpointEntryMatch & { - extraKey?: string; - } = getEndpointEntryMatchMock(); - payload.extraKey = 'some value'; - const decoded = endpointEntryMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts deleted file mode 100644 index 83e2a0f61bb4a8c..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; - -export const endpointEntryMatch = t.exact( - t.type({ - field: NonEmptyString, - operator: operatorIncluded, - type: t.keyof({ match: null }), - value: NonEmptyString, - }) -); -export type EndpointEntryMatch = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts deleted file mode 100644 index 95bd6008f1d7cd8..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants/index.mock'; -import { EndpointEntryMatchAny } from '.'; - -export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ - field: FIELD, - operator: OPERATOR, - type: MATCH_ANY, - value: [ENTRY_VALUE], -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts deleted file mode 100644 index 0fd878986d5a22e..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEndpointEntryMatchAnyMock } from './index.mock'; -import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock'; - -describe('endpointEntryMatchAny', () => { - test('it should validate an entry', () => { - const payload = getEndpointEntryMatchAnyMock(); - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate when operator is "excluded"', () => { - // Use the generic entry mock so we can test operator: excluded - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "excluded" supplied to "operator"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when field is empty string', () => { - const payload: Omit & { field: string } = { - ...getEndpointEntryMatchAnyMock(), - field: '', - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is empty array', () => { - const payload: Omit & { value: string[] } = { - ...getEndpointEntryMatchAnyMock(), - value: [], - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is not string array', () => { - const payload: Omit & { value: string } = { - ...getEndpointEntryMatchAnyMock(), - value: 'some string', - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match_any"', () => { - const payload: Omit & { type: string } = { - ...getEndpointEntryMatchAnyMock(), - type: 'match', - }; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EndpointEntryMatchAny & { - extraKey?: string; - } = getEndpointEntryMatchAnyMock(); - payload.extraKey = 'some extra key'; - const decoded = endpointEntryMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchAnyMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts deleted file mode 100644 index b39a428bb49dd7d..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { nonEmptyOrNullableStringArray } from '../../../non_empty_or_nullable_string_array'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; - -export const endpointEntryMatchAny = t.exact( - t.type({ - field: NonEmptyString, - operator: operatorIncluded, - type: t.keyof({ match_any: null }), - value: nonEmptyOrNullableStringArray, - }) -); -export type EndpointEntryMatchAny = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts deleted file mode 100644 index b66c5a2588eef49..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; - -export const endpointEntryMatchWildcard = t.exact( - t.type({ - field: NonEmptyString, - operator: operatorIncluded, - type: t.keyof({ wildcard: null }), - value: NonEmptyString, - }) -); -export type EndpointEntryMatchWildcard = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts deleted file mode 100644 index f59e29c8ce526b6..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EndpointEntryNested } from '.'; -import { FIELD, NESTED } from '../../../constants/index.mock'; -import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; -import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; - -export const getEndpointEntryNestedMock = (): EndpointEntryNested => ({ - entries: [getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock()], - field: FIELD, - type: NESTED, -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts deleted file mode 100644 index 03c02f67b71adb3..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { EndpointEntryNested, endpointEntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { getEndpointEntryNestedMock } from './index.mock'; -import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; -import { - nonEmptyEndpointNestedEntriesArray, - NonEmptyEndpointNestedEntriesArray, -} from '../non_empty_nested_entries_array'; -import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; -import { getEntryExistsMock } from '../../entries_exist/index.mock'; - -describe('endpointEntryNested', () => { - test('it should validate a nested entry', () => { - const payload = getEndpointEntryNestedMock(); - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "type" is not "nested"', () => { - const payload: Omit & { type: 'match' } = { - ...getEndpointEntryNestedMock(), - type: 'match', - }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { - field: string; - } = { ...getEndpointEntryNestedMock(), field: '' }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is not a string', () => { - const payload: Omit & { - field: number; - } = { ...getEndpointEntryNestedMock(), field: 1 }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "entries" is not an array', () => { - const payload: Omit & { - entries: string; - } = { ...getEndpointEntryNestedMock(), entries: 'im a string' }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "im a string" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEndpointEntryNestedMock(), entries: [getEndpointEntryMatchAnyMock()] }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - entries: [ - { - field: 'host.name', - operator: 'included', - type: 'match_any', - value: ['some host name'], - }, - ], - field: 'host.name', - type: 'nested', - }); - }); - - test('it should NOT validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEndpointEntryNestedMock(), entries: [getEntryExistsMock()] }; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "exists" supplied to "entries,type"', - 'Invalid value "undefined" supplied to "entries,value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EndpointEntryNested & { - extraKey?: string; - } = getEndpointEntryNestedMock(); - payload.extraKey = 'some extra key'; - const decoded = endpointEntryNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEndpointEntryNestedMock()); - }); - - test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { - const payload: NonEmptyEndpointNestedEntriesArray = [ - getEndpointEntryMatchMock(), - getEndpointEntryMatchAnyMock(), - ]; - const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); - expect(guarded).toBeTruthy(); - }); - - test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { - const payload: NonEmptyEndpointNestedEntriesArray = []; - const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); - expect(guarded).toBeFalsy(); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts deleted file mode 100644 index 249dcc9077b34d4..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../../non_empty_string'; -import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array'; - -export const endpointEntryNested = t.exact( - t.type({ - entries: nonEmptyEndpointNestedEntriesArray, - field: NonEmptyString, - type: t.keyof({ nested: null }), - }) -); -export type EndpointEntryNested = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts deleted file mode 100644 index b6e448f94ce6add..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryMatchMock } from '../entry_match/index.mock'; -import { entriesArray, entriesArrayOrUndefined, entry } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; -import { getEntryExistsMock } from '../entries_exist/index.mock'; -import { getEntryListMock } from '../entries_list/index.mock'; -import { getEntryNestedMock } from '../entry_nested/index.mock'; -import { getEntriesArrayMock } from './index.mock'; - -describe('Entries', () => { - describe('entry', () => { - test('it should validate a match entry', () => { - const payload = getEntryMatchMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a match_any entry', () => { - const payload = getEntryMatchAnyMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a exists entry', () => { - const payload = getEntryExistsMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a list entry', () => { - const payload = getEntryListMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation of nested entry', () => { - const payload = getEntryNestedMock(); - const decoded = entry.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - 'Invalid value "undefined" supplied to "list"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('entriesArray', () => { - test('it should validate an array with match entry', () => { - const payload = [getEntryMatchMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with match_any entry', () => { - const payload = [getEntryMatchAnyMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with exists entry', () => { - const payload = [getEntryExistsMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with list entry', () => { - const payload = [getEntryListMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with nested entry', () => { - const payload = [getEntryNestedMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with all types of entries', () => { - const payload = [...getEntriesArrayMock()]; - const decoded = entriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); - - describe('entriesArrayOrUndefined', () => { - test('it should validate undefined', () => { - const payload = undefined; - const decoded = entriesArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array with nested entry', () => { - const payload = [getEntryNestedMock()]; - const decoded = entriesArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts deleted file mode 100644 index 0882883f4d2398c..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EntryExists } from '.'; -import { EXISTS, FIELD, OPERATOR } from '../../constants/index.mock'; - -export const getEntryExistsMock = (): EntryExists => ({ - field: FIELD, - operator: OPERATOR, - type: EXISTS, -}); - -export const getEntryExistsExcludedMock = (): EntryExists => ({ - ...getEntryExistsMock(), - operator: 'excluded', -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts deleted file mode 100644 index db4edb54dfc2928..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryExistsMock } from './index.mock'; -import { entriesExists, EntryExists } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('entriesExists', () => { - test('it should validate an entry', () => { - const payload = getEntryExistsMock(); - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "included"', () => { - const payload = getEntryExistsMock(); - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryExistsMock(); - payload.operator = 'excluded'; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryExistsMock(), - field: '', - }; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryExists & { - extraKey?: string; - } = getEntryExistsMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryExistsMock()); - }); - - test('it should FAIL validation when "type" is not "exists"', () => { - const payload: Omit & { type: string } = { - ...getEntryExistsMock(), - type: 'match', - }; - const decoded = entriesExists.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts deleted file mode 100644 index 79c58944ea3f520..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -import { operator } from '../operator'; -import { NonEmptyString } from '../../non_empty_string'; - -export const entriesExists = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ exists: null }), - }) -); -export type EntryExists = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts deleted file mode 100644 index c4afb28f5ac54dc..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EntryList } from '.'; -import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants/index.mock'; - -export const getEntryListMock = (): EntryList => ({ - field: FIELD, - list: { id: LIST_ID, type: TYPE }, - operator: OPERATOR, - type: LIST, -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts deleted file mode 100644 index 2be3803c356de44..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryListMock } from './index.mock'; -import { entriesList, EntryList } from '.'; - -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('entriesList', () => { - test('it should validate an entry', () => { - const payload = getEntryListMock(); - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryListMock(); - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryListMock(); - payload.operator = 'excluded'; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "list" is not expected value', () => { - const payload: Omit & { list: string } = { - ...getEntryListMock(), - list: 'someListId', - }; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "someListId" supplied to "list"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "list.id" is empty string', () => { - const payload: Omit & { list: { id: string; type: 'ip' } } = { - ...getEntryListMock(), - list: { id: '', type: 'ip' }, - }; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "list,id"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "lists"', () => { - const payload: Omit & { type: 'match_any' } = { - ...getEntryListMock(), - type: 'match_any', - }; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "match_any" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryList & { - extraKey?: string; - } = getEntryListMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesList.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryListMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts deleted file mode 100644 index f5c662a67158b0b..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; - -import { type } from '../type'; -import { operator } from '../operator'; - -export const entriesList = t.exact( - t.type({ - field: NonEmptyString, - list: t.exact(t.type({ id: NonEmptyString, type })), - operator, - type: t.keyof({ list: null }), - }) -); -export type EntryList = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts deleted file mode 100644 index 4fdd8d915fe041f..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; - -export const getEntryMatchMock = (): EntryMatch => ({ - field: FIELD, - operator: OPERATOR, - type: MATCH, - value: ENTRY_VALUE, -}); - -export const getEntryMatchExcludeMock = (): EntryMatch => ({ - ...getEntryMatchMock(), - operator: 'excluded', -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts deleted file mode 100644 index 744c74c1223df56..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryMatchMock } from './index.mock'; -import { entriesMatch, EntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('entriesMatch', () => { - test('it should validate an entry', () => { - const payload = getEntryMatchMock(); - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryMatchMock(); - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryMatchMock(), - field: '', - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is not string', () => { - const payload: Omit & { value: string[] } = { - ...getEntryMatchMock(), - value: ['some value'], - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is empty string', () => { - const payload: Omit & { value: string } = { - ...getEntryMatchMock(), - value: '', - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match"', () => { - const payload: Omit & { type: string } = { - ...getEntryMatchMock(), - type: 'match_any', - }; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "match_any" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryMatch & { - extraKey?: string; - } = getEntryMatchMock(); - payload.extraKey = 'some value'; - const decoded = entriesMatch.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts deleted file mode 100644 index 668da1a3980933c..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { operator } from '../operator'; - -export const entriesMatch = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ match: null }), - value: NonEmptyString, - }) -); -export type EntryMatch = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts deleted file mode 100644 index 0022b00c604b02c..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EntryMatchAny } from '.'; -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; - -export const getEntryMatchAnyMock = (): EntryMatchAny => ({ - field: FIELD, - operator: OPERATOR, - type: MATCH_ANY, - value: [ENTRY_VALUE], -}); - -export const getEntryMatchAnyExcludeMock = (): EntryMatchAny => ({ - ...getEntryMatchAnyMock(), - operator: 'excluded', - value: [ENTRY_VALUE, 'some other host name'], -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts deleted file mode 100644 index 60fc4cdc26005fe..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryMatchAnyMock } from './index.mock'; -import { entriesMatchAny, EntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('entriesMatchAny', () => { - test('it should validate an entry', () => { - const payload = getEntryMatchAnyMock(); - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryMatchAnyMock(); - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "excluded"', () => { - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when field is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryMatchAnyMock(), - field: '', - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is empty array', () => { - const payload: Omit & { value: string[] } = { - ...getEntryMatchAnyMock(), - value: [], - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when value is not string array', () => { - const payload: Omit & { value: string } = { - ...getEntryMatchAnyMock(), - value: 'some string', - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "match_any"', () => { - const payload: Omit & { type: string } = { - ...getEntryMatchAnyMock(), - type: 'match', - }; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryMatchAny & { - extraKey?: string; - } = getEntryMatchAnyMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesMatchAny.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchAnyMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts deleted file mode 100644 index 4e7690a80f470ab..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -import { operator } from '../operator'; -import { nonEmptyOrNullableStringArray } from '../../non_empty_or_nullable_string_array'; -import { NonEmptyString } from '../../non_empty_string'; - -export const entriesMatchAny = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ match_any: null }), - value: nonEmptyOrNullableStringArray, - }) -); -export type EntryMatchAny = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts deleted file mode 100644 index 9810fe5e9875b54..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EntryMatchWildcard } from '.'; -import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants/index.mock'; - -export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ - field: FIELD, - operator: OPERATOR, - type: WILDCARD, - value: ENTRY_VALUE, -}); - -export const getEntryMatchWildcardExcludeMock = (): EntryMatchWildcard => ({ - ...getEntryMatchWildcardMock(), - operator: 'excluded', -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts deleted file mode 100644 index d9170dd60ab4058..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryMatchWildcardMock } from './index.mock'; -import { entriesMatchWildcard, EntryMatchWildcard } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('entriesMatchWildcard', () => { - test('it should validate an entry', () => { - const payload = getEntryMatchWildcardMock(); - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when operator is "included"', () => { - const payload = getEntryMatchWildcardMock(); - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when "operator" is "excluded"', () => { - const payload = getEntryMatchWildcardMock(); - payload.operator = 'excluded'; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { field: string } = { - ...getEntryMatchWildcardMock(), - field: '', - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is not string', () => { - const payload: Omit & { value: string[] } = { - ...getEntryMatchWildcardMock(), - value: ['some value'], - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["some value"]" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "value" is empty string', () => { - const payload: Omit & { value: string } = { - ...getEntryMatchWildcardMock(), - value: '', - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "type" is not "wildcard"', () => { - const payload: Omit & { type: string } = { - ...getEntryMatchWildcardMock(), - type: 'match', - }; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: EntryMatchWildcard & { - extraKey?: string; - } = getEntryMatchWildcardMock(); - payload.extraKey = 'some value'; - const decoded = entriesMatchWildcard.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryMatchWildcardMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts deleted file mode 100644 index 100b0c665d91bf6..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { operator } from '../operator'; - -export const entriesMatchWildcard = t.exact( - t.type({ - field: NonEmptyString, - operator, - type: t.keyof({ wildcard: null }), - value: NonEmptyString, - }) -); -export type EntryMatchWildcard = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts deleted file mode 100644 index acde4443cccb754..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EntryNested } from '.'; -import { NESTED, NESTED_FIELD } from '../../constants/index.mock'; -import { getEntryExistsMock } from '../entries_exist/index.mock'; -import { getEntryMatchExcludeMock, getEntryMatchMock } from '../entry_match/index.mock'; -import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from '../entry_match_any/index.mock'; - -export const getEntryNestedMock = (): EntryNested => ({ - entries: [getEntryMatchMock(), getEntryMatchAnyMock()], - field: NESTED_FIELD, - type: NESTED, -}); - -export const getEntryNestedExcludeMock = (): EntryNested => ({ - ...getEntryNestedMock(), - entries: [getEntryMatchExcludeMock(), getEntryMatchAnyExcludeMock()], -}); - -export const getEntryNestedMixedEntries = (): EntryNested => ({ - ...getEntryNestedMock(), - entries: [getEntryMatchMock(), getEntryMatchAnyExcludeMock(), getEntryExistsMock()], -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts deleted file mode 100644 index b6bbc4dbef4a339..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEntryNestedMock } from './index.mock'; -import { entriesNested, EntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; -import { getEntryExistsMock } from '../entries_exist/index.mock'; - -describe('entriesNested', () => { - test('it should validate a nested entry', () => { - const payload = getEntryNestedMock(); - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when "type" is not "nested"', () => { - const payload: Omit & { type: 'match' } = { - ...getEntryNestedMock(), - type: 'match', - }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is empty string', () => { - const payload: Omit & { - field: string; - } = { ...getEntryNestedMock(), field: '' }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "field" is not a string', () => { - const payload: Omit & { - field: number; - } = { ...getEntryNestedMock(), field: 1 }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when "entries" is not a an array', () => { - const payload: Omit & { - entries: string; - } = { ...getEntryNestedMock(), entries: 'im a string' }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "im a string" supplied to "entries"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - entries: [ - { - field: 'host.name', - operator: 'included', - type: 'match_any', - value: ['some host name'], - }, - ], - field: 'parent.field', - type: 'nested', - }); - }); - - test('it should validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - entries: [ - { - field: 'host.name', - operator: 'included', - type: 'exists', - }, - ], - field: 'parent.field', - type: 'nested', - }); - }); - - test('it should strip out extra keys', () => { - const payload: EntryNested & { - extraKey?: string; - } = getEntryNestedMock(); - payload.extraKey = 'some extra key'; - const decoded = entriesNested.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getEntryNestedMock()); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts deleted file mode 100644 index ff224dd836a19df..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { nonEmptyNestedEntriesArray } from '../non_empty_nested_entries_array'; - -export const entriesNested = t.exact( - t.type({ - entries: nonEmptyNestedEntriesArray, - field: NonEmptyString, - type: t.keyof({ nested: null }), - }) -); -export type EntryNested = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts deleted file mode 100644 index 652395fa651ea02..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -export * from './comment'; -export * from './create_comment'; -export * from './default_comments_array'; -export * from './default_comments_array'; -export * from './default_namespace'; -export * from './default_namespace_array'; -export * from './default_update_comments_array'; -export * from './entries'; -export * from './entries_exist'; -export * from './entries_list'; -export * from './entry_match'; -export * from './entry_match_any'; -export * from './entry_match_wildcard'; -export * from './entry_nested'; -export * from './exception_list'; -export * from './exception_list_item_type'; -export * from './item_id'; -export * from './lists'; -export * from './lists_default_array'; -export * from './non_empty_entries_array'; -export * from './non_empty_nested_entries_array'; -export * from './operator'; -export * from './os_type'; -export * from './type'; -export * from './update_comment'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts deleted file mode 100644 index 171db8fd60fd143..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; - -export const item_id = NonEmptyString; -export type ItemId = t.TypeOf; -export const itemIdOrUndefined = t.union([item_id, t.undefined]); -export type ItemIdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts deleted file mode 100644 index c6f54b57d937b03..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { List, ListArray } from '.'; -import { ENDPOINT_LIST_ID } from '../../constants'; - -export const getListMock = (): List => ({ - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', -}); - -export const getEndpointListMock = (): List => ({ - id: ENDPOINT_LIST_ID, - list_id: ENDPOINT_LIST_ID, - namespace_type: 'agnostic', - type: 'endpoint', -}); - -export const getListArrayMock = (): ListArray => [getListMock(), getEndpointListMock()]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts deleted file mode 100644 index 77d5e72ef8bc8ea..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getEndpointListMock, getListArrayMock, getListMock } from './index.mock'; -import { List, list, ListArray, listArray, ListArrayOrUndefined, listArrayOrUndefined } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('Lists', () => { - describe('list', () => { - test('it should validate a list', () => { - const payload = getListMock(); - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a list with "namespace_type" of "agnostic"', () => { - const payload = getEndpointListMock(); - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a list without an "id"', () => { - const payload = getListMock(); - // @ts-expect-error - delete payload.id; - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a list without "namespace_type"', () => { - const payload = getListMock(); - // @ts-expect-error - delete payload.namespace_type; - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "namespace_type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: List & { - extraKey?: string; - } = getListMock(); - payload.extraKey = 'some value'; - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getListMock()); - }); - }); - - describe('listArray', () => { - test('it should validate an array of lists', () => { - const payload = getListArrayMock(); - const decoded = listArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate when unexpected type found in array', () => { - const payload = ([1] as unknown) as ListArray; - const decoded = listArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('listArrayOrUndefined', () => { - test('it should validate an array of lists', () => { - const payload = getListArrayMock(); - const decoded = listArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when undefined', () => { - const payload = undefined; - const decoded = listArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not allow an item that is not of type "list" in array', () => { - const payload = ([1] as unknown) as ListArrayOrUndefined; - const decoded = listArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', - 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts deleted file mode 100644 index 1bd1806564856b3..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { exceptionListType } from '../exception_list'; -import { namespaceType } from '../default_namespace'; -import { NonEmptyString } from '../../non_empty_string'; - -export const list = t.exact( - t.type({ - id: NonEmptyString, - list_id: NonEmptyString, - type: exceptionListType, - namespace_type: namespaceType, - }) -); - -export type List = t.TypeOf; -export const listArray = t.array(list); -export type ListArray = t.TypeOf; -export const listArrayOrUndefined = t.union([listArray, t.undefined]); -export type ListArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts deleted file mode 100644 index 03d16d8e1b5ca8c..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultListArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getListArrayMock } from '../lists/index.mock'; - -describe('lists_default_array', () => { - test('it should return a default array when null', () => { - const payload = null; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should return a default array when undefined', () => { - const payload = undefined; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of lists', () => { - const payload = getListArrayMock(); - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array of non accepted types', () => { - // Terrible casting for purpose of tests - const payload = [1] as unknown; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "DefaultListArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts deleted file mode 100644 index 11e6e54b344a9dc..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { EntriesArray } from '../entries'; -import { nonEmptyEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getEntryMatchMock } from '../entry_match/index.mock'; -import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; -import { getEntryExistsMock } from '../entries_exist/index.mock'; -import { - getEntriesArrayMock, - getListAndNonListEntriesArrayMock, - getListEntriesArrayMock, -} from '../entries/index.mock'; -import { getEntryNestedMock } from '../entry_nested/index.mock'; - -describe('non_empty_entries_array', () => { - test('it should FAIL validation when given an empty array', () => { - const payload: EntriesArray = []; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "undefined"', () => { - const payload = undefined; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "null"', () => { - const payload = null; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "list" entries', () => { - const payload: EntriesArray = [...getListEntriesArrayMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "nested" entries', () => { - const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of entries', () => { - const payload: EntriesArray = [...getEntriesArrayMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { - const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Cannot have entry of type list and other']); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given an array of non entries', () => { - const payload = [1]; - const decoded = nonEmptyEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "NonEmptyEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts deleted file mode 100644 index 95b74a6d4fe4300..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { EntriesArray } from '../entries'; -import { nonEmptyNestedEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { getEntryMatchMock } from '../entry_match/index.mock'; -import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; -import { getEntryExistsMock } from '../entries_exist/index.mock'; -import { getEntryNestedMock } from '../entry_nested/index.mock'; - -describe('non_empty_nested_entries_array', () => { - test('it should FAIL validation when given an empty array', () => { - const payload: EntriesArray = []; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "undefined"', () => { - const payload = undefined; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "null"', () => { - const payload = null; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when given an array of "nested" entries', () => { - const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "operator"', - 'Invalid value "nested" supplied to "type"', - 'Invalid value "undefined" supplied to "value"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of entries', () => { - const payload: EntriesArray = [ - getEntryExistsMock(), - getEntryMatchAnyMock(), - getEntryMatchMock(), - ]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should FAIL validation when given an array of non entries', () => { - const payload = [1]; - const decoded = nonEmptyNestedEntriesArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts deleted file mode 100644 index 7fe2b6e4d8ba79d..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -export const operator = t.keyof({ excluded: null, included: null }); -export type Operator = t.TypeOf; -export enum OperatorEnum { - INCLUDED = 'included', - EXCLUDED = 'excluded', -} - -export enum OperatorTypeEnum { - NESTED = 'nested', - MATCH = 'match', - MATCH_ANY = 'match_any', - WILDCARD = 'wildcard', - EXISTS = 'exists', - LIST = 'list', -} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts deleted file mode 100644 index 5ff60e05817d5e3..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { DefaultArray } from '../../default_array'; - -export const osType = t.keyof({ - linux: null, - macos: null, - windows: null, -}); -export type OsType = t.TypeOf; - -export const osTypeArray = DefaultArray(osType); -export type OsTypeArray = t.TypeOf; - -export const osTypeArrayOrUndefined = t.union([osTypeArray, t.undefined]); -export type OsTypeArrayOrUndefined = t.OutputOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts deleted file mode 100644 index 0eebf2eeaace127..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -/** - * Types of all the regular single value list items but not exception list - * or exception list types. Those types are in the list_types folder. - */ -export const type = t.keyof({ - binary: null, - boolean: null, - byte: null, - date: null, - date_nanos: null, - date_range: null, - double: null, - double_range: null, - float: null, - float_range: null, - geo_point: null, - geo_shape: null, - half_float: null, - integer: null, - integer_range: null, - ip: null, - ip_range: null, - keyword: null, - long: null, - long_range: null, - shape: null, - short: null, - text: null, -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts deleted file mode 100644 index 3b5cb256b28bfa7..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { UpdateComment, UpdateCommentsArray } from '.'; -import { ID } from '../../constants/index.mock'; - -export const getUpdateCommentMock = (): UpdateComment => ({ - comment: 'some comment', - id: ID, -}); - -export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [ - getUpdateCommentMock(), - getUpdateCommentMock(), -]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts deleted file mode 100644 index a6fc285f05465eb..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './index.mock'; -import { - UpdateComment, - updateComment, - UpdateCommentsArray, - updateCommentsArray, - UpdateCommentsArrayOrUndefined, - updateCommentsArrayOrUndefined, -} from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; - -describe('CommentsUpdate', () => { - describe('updateComment', () => { - test('it should pass validation when supplied typical comment update', () => { - const payload = getUpdateCommentMock(); - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an undefined for "comment"', () => { - const payload = getUpdateCommentMock(); - // @ts-expect-error - delete payload.comment; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "comment"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when supplied an empty string for "comment"', () => { - const payload = { ...getUpdateCommentMock(), comment: '' }; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); - expect(message.schema).toEqual({}); - }); - - test('it should pass validation when supplied an undefined for "id"', () => { - const payload = getUpdateCommentMock(); - delete payload.id; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when supplied an empty string for "id"', () => { - const payload = { ...getUpdateCommentMock(), id: '' }; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "id"']); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra key passed in', () => { - const payload: UpdateComment & { - extraKey?: string; - } = { ...getUpdateCommentMock(), extraKey: 'some new value' }; - const decoded = updateComment.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getUpdateCommentMock()); - }); - }); - - describe('updateCommentsArray', () => { - test('it should pass validation when supplied an array of comments', () => { - const payload = getUpdateCommentsArrayMock(); - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when undefined', () => { - const payload = undefined; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when array includes non comments types', () => { - const payload = ([1] as unknown) as UpdateCommentsArray; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('updateCommentsArrayOrUndefined', () => { - test('it should pass validation when supplied an array of comments', () => { - const payload = getUpdateCommentsArrayMock(); - const decoded = updateCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should pass validation when supplied when undefined', () => { - const payload = undefined; - const decoded = updateCommentsArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation when array includes non comments types', () => { - const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; - const decoded = updateCommentsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts deleted file mode 100644 index 496ff07c5616f29..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { id } from '../../id'; - -export const updateComment = t.intersection([ - t.exact( - t.type({ - comment: NonEmptyString, - }) - ), - t.exact( - t.partial({ - id, - }) - ), -]); - -export type UpdateComment = t.TypeOf; -export const updateCommentsArray = t.array(updateComment); -export type UpdateCommentsArray = t.TypeOf; -export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); -export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts deleted file mode 100644 index 4c68cb01cf00f41..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; - -export const max_signals = PositiveIntegerGreaterThanZero; -export type MaxSignals = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts deleted file mode 100644 index 0ea7eb5539ba95b..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { NonEmptyArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -const testSchema = t.keyof({ - valid: true, - also_valid: true, -}); -type TestSchema = t.TypeOf; - -const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray'); - -describe('non empty array', () => { - test('it should generate the correct name for non empty array', () => { - const newTestSchema = NonEmptyArray(testSchema); - expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">'); - }); - - test('it should use a supplied name override', () => { - const newTestSchema = NonEmptyArray(testSchema, 'someName'); - expect(newTestSchema.name).toEqual('someName'); - }); - - test('it should NOT validate an empty array', () => { - const payload: string[] = []; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of testSchema', () => { - const payload: TestSchema[] = ['valid']; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of valid testSchema strings', () => { - const payload: TestSchema[] = ['valid', 'also_valid']; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['valid', 123]; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "123" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an array with an invalid string', () => { - const payload = ['valid', 'invalid']; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a null value', () => { - const payload = null; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts deleted file mode 100644 index fb2e91862d91e2a..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { nonEmptyOrNullableStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('nonEmptyOrNullableStringArray', () => { - test('it should FAIL validation when given an empty array', () => { - const payload: string[] = []; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "undefined"', () => { - const payload = undefined; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given "null"', () => { - const payload = null; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given an array of with an empty string', () => { - const payload: string[] = ['im good', '']; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "["im good",""]" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should FAIL validation when given an array of non strings', () => { - const payload = [1]; - const decoded = nonEmptyOrNullableStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[1]" supplied to "NonEmptyOrNullableStringArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts deleted file mode 100644 index 15c8ced8c915f5d..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { NonEmptyString } from '.'; - -describe('non_empty_string', () => { - test('it should validate a regular string', () => { - const payload = '1'; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "NonEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "" supplied to "NonEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate empty spaces', () => { - const payload = ' '; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value " " supplied to "NonEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts deleted file mode 100644 index 6c7eb0ae33ab9e4..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; - -import { NonEmptyArray } from '../non_empty_array'; - -export const machine_learning_job_id_normalized = NonEmptyArray(t.string); -export type MachineLearningJobIdNormalized = t.TypeOf; - -export const machineLearningJobIdNormalizedOrUndefined = t.union([ - machine_learning_job_id_normalized, - t.undefined, -]); -export type MachineLearningJobIdNormalizedOrUndefined = t.TypeOf< - typeof machineLearningJobIdNormalizedOrUndefined ->; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts deleted file mode 100644 index 7f06ec2153a50a6..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { OnlyFalseAllowed } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('only_false_allowed', () => { - test('it should validate a boolean false as false', () => { - const payload = false; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a boolean true', () => { - const payload = true; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "true" supplied to "DefaultBooleanTrue"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultBooleanTrue"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts deleted file mode 100644 index d8aa9d29395893d..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; -import dateMath from '@elastic/datemath'; - -/** - * TODO: Move this to kbn-securitysolution-utils - * @deprecated Use the parseScheduleDates from the kbn-securitysolution-utils. - */ -export const parseScheduleDates = (time: string): moment.Moment | null => { - const isValidDateString = !isNaN(Date.parse(time)); - const isValidInput = isValidDateString || time.trim().startsWith('now'); - const formattedDate = isValidDateString - ? moment(time) - : isValidInput - ? dateMath.parse(time) - : null; - - return formattedDate ?? null; -}; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts deleted file mode 100644 index c6c841b746089d1..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { PositiveInteger } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('positive_integer_greater_than_zero', () => { - test('it should validate a positive number', () => { - const payload = 1; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a zero', () => { - const payload = 0; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a negative number', () => { - const payload = -1; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveInteger"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a string', () => { - const payload = 'some string'; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "PositiveInteger"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts deleted file mode 100644 index 4655207a6448e36..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('positive_integer_greater_than_zero', () => { - test('it should validate a positive number', () => { - const payload = 1; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a zero', () => { - const payload = 0; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a negative number', () => { - const payload = -1; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a string', () => { - const payload = 'some string'; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts deleted file mode 100644 index 41754a7ce0606d7..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultStringArray } from '../default_string_array'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_string_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of strings', () => { - const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts deleted file mode 100644 index bca8b92134928b0..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { RiskScore } from '.'; - -describe('risk_score', () => { - test('it should validate a positive number', () => { - const payload = 1; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a zero', () => { - const payload = 0; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a negative number', () => { - const payload = -1; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "RiskScore"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a string', () => { - const payload = 'some string'; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "RiskScore"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a risk score greater than 100', () => { - const payload = 101; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "101" supplied to "RiskScore"']); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts deleted file mode 100644 index 0aca7dd70ba1d64..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the risk score as: - * - Natural Number (positive integer and not a float), - * - Between the values [0 and 100] inclusive. - */ -export const RiskScore = new t.Type( - 'RiskScore', - t.number.is, - (input, context): Either => { - return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 && input <= 100 - ? t.success(input) - : t.failure(input, context); - }, - t.identity -); - -export type RiskScoreC = typeof RiskScore; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts deleted file mode 100644 index 1d7ca20e80b3b6e..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { RiskScore } from '../risk_score'; - -import { operator } from '../operator'; - -export const riskScoreOrUndefined = t.union([RiskScore, t.undefined]); -export type RiskScoreOrUndefined = t.TypeOf; - -export const risk_score_mapping_field = t.string; -export const risk_score_mapping_value = t.string; -export const risk_score_mapping_item = t.exact( - t.type({ - field: risk_score_mapping_field, - value: risk_score_mapping_value, - operator, - risk_score: riskScoreOrUndefined, - }) -); - -export const risk_score_mapping = t.array(risk_score_mapping_item); -export type RiskScoreMapping = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts deleted file mode 100644 index 9e7ee7d2831cd62..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; - -import { operator } from '../operator'; -import { severity } from '../severity'; - -export const severity_mapping_field = t.string; -export const severity_mapping_value = t.string; -export const severity_mapping_item = t.exact( - t.type({ - field: severity_mapping_field, - operator, - value: severity_mapping_value, - severity, - }) -); -export type SeverityMappingItem = t.TypeOf; - -export const severity_mapping = t.array(severity_mapping_item); -export type SeverityMapping = t.TypeOf; - -export const severityMappingOrUndefined = t.union([severity_mapping, t.undefined]); -export type SeverityMappingOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts deleted file mode 100644 index 48bcca0551352e6..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; - -import { DefaultStringArray } from '../default_string_array'; - -export const tags = DefaultStringArray; -export type Tags = t.TypeOf; -export const tagsOrUndefined = t.union([tags, t.undefined]); -export type TagsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts deleted file mode 100644 index 0e4022e3ec26e91..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { threat_tactic } from '../threat_tactic'; -import { threat_techniques } from '../threat_technique'; - -export const threat_framework = t.string; - -export const threat = t.intersection([ - t.exact( - t.type({ - framework: threat_framework, - tactic: threat_tactic, - }) - ), - t.exact( - t.partial({ - technique: threat_techniques, - }) - ), -]); - -export type Threat = t.TypeOf; - -export const threats = t.array(threat); -export type Threats = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts deleted file mode 100644 index 7f754fb2d87de45..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { - concurrent_searches, - items_per_search, - ThreatMapping, - threatMappingEntries, - ThreatMappingEntries, - threat_mapping, -} from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { exactCheck } from '../exact_check'; - -describe('threat_mapping', () => { - describe('threatMappingEntries', () => { - test('it should validate an entry', () => { - const payload: ThreatMappingEntries = [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - }, - ]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation with an extra entry item', () => { - const payload: ThreatMappingEntries & Array<{ extra: string }> = [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - extra: 'blah', - }, - ]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation with a non string', () => { - const payload = ([ - { - field: 5, - type: 'mapping', - value: 'field.one', - }, - ] as unknown) as ThreatMappingEntries[]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation with a wrong type', () => { - const payload = ([ - { - field: 'field.one', - type: 'invalid', - value: 'field.one', - }, - ] as unknown) as ThreatMappingEntries[]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('threat_mapping', () => { - test('it should validate a threat mapping', () => { - const payload: ThreatMapping = [ - { - entries: [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - }, - ], - }, - ]; - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); - - test('it should fail validate with an extra key', () => { - const payload: ThreatMapping & Array<{ extra: string }> = [ - { - entries: [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - }, - ], - extra: 'invalid', - }, - ]; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validate with an extra inner entry', () => { - const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [ - { - entries: [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - extra: 'blah', - }, - ], - }, - ]; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validate with an extra inner entry with the wrong data type', () => { - const payload = ([ - { - entries: [ - { - field: 5, - type: 'mapping', - value: 'field.one', - }, - ], - }, - ] as unknown) as ThreatMapping; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "entries,field"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validate with empty array', () => { - const payload: string[] = []; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when concurrent_searches is < 0', () => { - const payload = -1; - const decoded = concurrent_searches.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when concurrent_searches is 0', () => { - const payload = 0; - const decoded = concurrent_searches.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when items_per_search is 0', () => { - const payload = 0; - const decoded = items_per_search.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when items_per_search is < 0', () => { - const payload = -1; - const decoded = items_per_search.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts deleted file mode 100644 index 4fc64fe1e098220..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { language } from '../language'; -import { NonEmptyArray } from '../non_empty_array'; -import { NonEmptyString } from '../non_empty_string'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; - -export const threat_query = t.string; -export type ThreatQuery = t.TypeOf; -export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); -export type ThreatQueryOrUndefined = t.TypeOf; - -export const threat_indicator_path = t.string; -export type ThreatIndicatorPath = t.TypeOf; -export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); -export type ThreatIndicatorPathOrUndefined = t.TypeOf; - -export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet -export type ThreatFilters = t.TypeOf; -export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); -export type ThreatFiltersOrUndefined = t.TypeOf; - -export const threatMapEntry = t.exact( - t.type({ - field: NonEmptyString, - type: t.keyof({ mapping: null }), - value: NonEmptyString, - }) -); - -export type ThreatMapEntry = t.TypeOf; - -export const threatMappingEntries = t.array(threatMapEntry); -export type ThreatMappingEntries = t.TypeOf; - -export const threatMap = t.exact( - t.type({ - entries: threatMappingEntries, - }) -); -export type ThreatMap = t.TypeOf; - -export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); -export type ThreatMapping = t.TypeOf; - -export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); -export type ThreatMappingOrUndefined = t.TypeOf; - -export const threat_index = t.array(t.string); -export type ThreatIndex = t.TypeOf; -export const threatIndexOrUndefined = t.union([threat_index, t.undefined]); -export type ThreatIndexOrUndefined = t.TypeOf; - -export const threat_language = t.union([language, t.undefined]); -export type ThreatLanguage = t.TypeOf; -export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]); -export type ThreatLanguageOrUndefined = t.TypeOf; - -export const concurrent_searches = PositiveIntegerGreaterThanZero; -export type ConcurrentSearches = t.TypeOf; -export const concurrentSearchesOrUndefined = t.union([concurrent_searches, t.undefined]); -export type ConcurrentSearchesOrUndefined = t.TypeOf; - -export const items_per_search = PositiveIntegerGreaterThanZero; -export type ItemsPerSearch = t.TypeOf; -export const itemsPerSearchOrUndefined = t.union([items_per_search, t.undefined]); -export type ItemsPerSearchOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts deleted file mode 100644 index 8d64f53cb162308..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; - -export const threat_subtechnique_id = t.string; -export const threat_subtechnique_name = t.string; -export const threat_subtechnique_reference = t.string; - -export const threat_subtechnique = t.type({ - id: threat_subtechnique_id, - name: threat_subtechnique_name, - reference: threat_subtechnique_reference, -}); - -export const threat_subtechniques = t.array(threat_subtechnique); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts deleted file mode 100644 index ed2e771e1e118fb..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { threat_subtechniques } from '../threat_subtechnique'; - -export const threat_technique_id = t.string; -export const threat_technique_name = t.string; -export const threat_technique_reference = t.string; - -export const threat_technique = t.intersection([ - t.exact( - t.type({ - id: threat_technique_id, - name: threat_technique_name, - reference: threat_technique_reference, - }) - ), - t.exact( - t.partial({ - subtechnique: threat_subtechniques, - }) - ), -]); -export const threat_techniques = t.array(threat_technique); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts deleted file mode 100644 index e8214ac60313f23..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { UUID } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('uuid', () => { - test('it should validate a uuid', () => { - const payload = '4656dc92-5832-11ea-8e2d-0242ac130003'; - const decoded = UUID.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a non uuid', () => { - const payload = '4656dc92-5832-11ea-8e2d'; - const decoded = UUID.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "4656dc92-5832-11ea-8e2d" supplied to "UUID"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = UUID.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "UUID"']); - expect(message.schema).toEqual({}); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts deleted file mode 100644 index 38cb47ebce53e53..000000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; - -/** - * Note this is just a positive number, but we use it as a type here which is still ok. - * This type was originally from "x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts" - * but is moved here to make things more portable. No unit tests, but see PositiveIntegerGreaterThanZero integer for unit tests. - */ -export const version = PositiveIntegerGreaterThanZero; -export type Version = t.TypeOf; diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index 33c25a3091c2405..42897e93593b6a1 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -35,7 +35,7 @@ SRC_DEPS = [ TYPES_DEPS = [ "@npm//@types/jest", "@npm//@types/node", - "@npm//@types/uuid" + "@npm//@types/uuid" ] DEPS = SRC_DEPS + TYPES_DEPS @@ -83,4 +83,4 @@ filegroup( ":npm_module", ], visibility = ["//visibility:public"], -) \ No newline at end of file +) diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel new file mode 100644 index 000000000000000..61570969c85f14d --- /dev/null +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -0,0 +1,90 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-server-http-tools" +PKG_REQUIRE_NAME = "@kbn/server-http-tools" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "//packages/kbn-crypto", + "@npm//@hapi/hapi", + "@npm//@hapi/hoek", + "@npm//joi", + "@npm//moment", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/hapi__hapi", + "@npm//@types/joi", + "@npm//@types/node", + "@npm//@types/uuid", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json index c44bf17079aab5e..7ec52743f027e3d 100644 --- a/packages/kbn-server-http-tools/package.json +++ b/packages/kbn-server-http-tools/package.json @@ -4,13 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "rm -rf target && ../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - }, - "devDependencies": { - "@kbn/utility-types": "link:../kbn-utility-types" - } + "private": true } diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index 2f3e4626a04ce79..034cbd233491909 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-server-http-tools/src" }, diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel new file mode 100644 index 000000000000000..4d1b4f21117c4d5 --- /dev/null +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -0,0 +1,97 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-telemetry-tools" +PKG_REQUIRE_NAME = "@kbn/telemetry-tools" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixture__/**", + "**/__snapshots__/**", + ] +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", + "GUIDELINE.md", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-utility-types", + "@npm//glob", + "@npm//jest-styled-components", + "@npm//listr", + "@npm//normalize-path", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/glob", + "@npm//@types/jest", + "@npm//@types/listr", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/normalize-path", + "@npm//@types/testing-library__jest-dom", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-telemetry-tools/babel.config.js b/packages/kbn-telemetry-tools/babel.config.js deleted file mode 100644 index 68c67e549b74bad..000000000000000 --- a/packages/kbn-telemetry-tools/babel.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - presets: ['@kbn/babel-preset/node_preset'], - ignore: ['**/*.test.ts', '**/__fixture__/**'], -}; diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 31fac5c04383202..cd3fd3aa966c772 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -7,10 +7,5 @@ "private": true, "kibana": { "devOnly": true - }, - "scripts": { - "build": "../../node_modules/.bin/babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 419af1d02f83b59..926ebff17f63903 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", "isolatedModules": true diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 17ce97528551d4e..275d9fac73c58d4 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/i18n": "link:../kbn-i18n", "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index e802613fbaedb50..658fc9382d61634 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -36,7 +36,7 @@ interface TestClusterFactoryOptions { * */ dataArchive?: string; esArgs?: string[]; - esEnvVars?: Record; + esJavaOpts?: string; clusterName?: string; log: ToolingLog; ssl?: boolean; @@ -52,7 +52,7 @@ export function createTestEsCluster(options: TestClusterFactoryOptions) { esFrom = esTestConfig.getBuildFrom(), dataArchive, esArgs: customEsArgs = [], - esEnvVars, + esJavaOpts, clusterName: customClusterName = 'es-test-cluster', ssl, } = options; @@ -107,7 +107,7 @@ export function createTestEsCluster(options: TestClusterFactoryOptions) { await cluster.start(installPath, { password: config.password, esArgs, - esEnvVars, + esJavaOpts, }); } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index d82b7b83e8f151d..65573fdbd66476e 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -172,7 +172,7 @@ export const schema = Joi.object() license: Joi.string().default('basic'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), - serverEnvVars: Joi.object(), + esJavaOpts: Joi.string(), dataArchive: Joi.string(), ssl: Joi.boolean().default(false), }) diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 83368783da38971..7ba9a3c1c4733e4 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -29,7 +29,7 @@ export async function runElasticsearch({ const ssl = config.get('esTestCluster.ssl'); const license = config.get('esTestCluster.license'); const esArgs = config.get('esTestCluster.serverArgs'); - const esEnvVars = config.get('esTestCluster.serverEnvVars'); + const esJavaOpts = config.get('esTestCluster.esJavaOpts'); const isSecurityEnabled = esArgs.includes('xpack.security.enabled=true'); const cluster = createTestEsCluster({ @@ -43,7 +43,7 @@ export async function runElasticsearch({ esFrom: esFrom || config.get('esTestCluster.from'), dataArchive: config.get('esTestCluster.dataArchive'), esArgs, - esEnvVars, + esJavaOpts, ssl, }); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 54d983bf1bf4410..c284be4487a5f0f 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,6 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@kbn/i18n": "link:../kbn-i18n", "@kbn/monaco": "link:../kbn-monaco" } } \ No newline at end of file diff --git a/rfcs/images/0018_agent_manager.png b/rfcs/images/0018_agent_manager.png new file mode 100644 index 000000000000000..92a135386e258bf Binary files /dev/null and b/rfcs/images/0018_agent_manager.png differ diff --git a/rfcs/images/0018_buildkite_build.png b/rfcs/images/0018_buildkite_build.png new file mode 100644 index 000000000000000..1b04fc85561c8db Binary files /dev/null and b/rfcs/images/0018_buildkite_build.png differ diff --git a/rfcs/images/0018_buildkite_deps.png b/rfcs/images/0018_buildkite_deps.png new file mode 100644 index 000000000000000..f4b0376d6e26f54 Binary files /dev/null and b/rfcs/images/0018_buildkite_deps.png differ diff --git a/rfcs/images/0018_buildkite_uptime.png b/rfcs/images/0018_buildkite_uptime.png new file mode 100644 index 000000000000000..f850df50b0f1b59 Binary files /dev/null and b/rfcs/images/0018_buildkite_uptime.png differ diff --git a/rfcs/images/0018_jenkins_pipeline_steps.png b/rfcs/images/0018_jenkins_pipeline_steps.png new file mode 100644 index 000000000000000..51ad8fde8aae0ee Binary files /dev/null and b/rfcs/images/0018_jenkins_pipeline_steps.png differ diff --git a/rfcs/images/timeslider/toolbar.png b/rfcs/images/timeslider/toolbar.png new file mode 100644 index 000000000000000..3dae2af0d77f72a Binary files /dev/null and b/rfcs/images/timeslider/toolbar.png differ diff --git a/rfcs/images/timeslider/v1.png b/rfcs/images/timeslider/v1.png new file mode 100644 index 000000000000000..4766fa24c816a91 Binary files /dev/null and b/rfcs/images/timeslider/v1.png differ diff --git a/rfcs/images/timeslider/v2.png b/rfcs/images/timeslider/v2.png new file mode 100644 index 000000000000000..e4a77e241dc8373 Binary files /dev/null and b/rfcs/images/timeslider/v2.png differ diff --git a/rfcs/text/0018_buildkite.md b/rfcs/text/0018_buildkite.md new file mode 100644 index 000000000000000..560540ae3af0b8e --- /dev/null +++ b/rfcs/text/0018_buildkite.md @@ -0,0 +1,923 @@ +- Start Date: 2021-03-29 +- RFC PR: [#95070](https://github.com/elastic/kibana/pull/95070) +- Kibana Issue: [#94630](https://github.com/elastic/kibana/issues/94630) + +--- + +- [Summary](#summary) +- [Motivation](#motivation) + - [Required and Desired Capabilities](#required-and-desired-capabilities) + - [Required](#required) + - [Scalable](#scalable) + - [Stable](#stable) + - [Surfaces information intuitively](#surfaces-information-intuitively) + - [Pipelines](#pipelines) + - [Advanced Pipeline logic](#advanced-pipeline-logic) + - [Cloud-friendly pricing model](#cloud-friendly-pricing-model) + - [Public access](#public-access) + - [Secrets handling](#secrets-handling) + - [Support or Documentation](#support-or-documentation) + - [Scheduled Builds](#scheduled-builds) + - [Container support](#container-support) + - [Desired](#desired) + - [Customization](#customization) + - [Core functionality is first-party](#core-functionality-is-first-party) + - [First-class support for test results](#first-class-support-for-test-results) + - [GitHub Integration](#github-integration) +- [Buildkite - Detailed design](#buildkite---detailed-design) + - [Overview](#overview) + - [Required and Desired Capabilities](#required-and-desired-capabilities-1) + - [Required](#required-1) + - [Scalable](#scalable-1) + - [Stable](#stable-1) + - [Surfaces information intuitively](#surfaces-information-intuitively-1) + - [Pipelines](#pipelines-1) + - [Advanced Pipeline logic](#advanced-pipeline-logic-1) + - [Cloud-friendly pricing model](#cloud-friendly-pricing-model-1) + - [Public access](#public-access-1) + - [Secrets handling](#secrets-handling-1) + - [Support or Documentation](#support-or-documentation-1) + - [Scheduled Builds](#scheduled-builds-1) + - [Container support](#container-support-1) + - [Desired](#desired-1) + - [Customization](#customization-1) + - [Core functionality is first-party](#core-functionality-is-first-party-1) + - [First-class support for test results](#first-class-support-for-test-results-1) + - [GitHub Integration](#github-integration-1) + - [What we will build and manage](#what-we-will-build-and-manage) + - [Elastic Buildkite Agent Manager](#elastic-buildkite-agent-manager) + - [Overview](#overview-1) + - [Design](#design) + - [Protection against creating too many instances](#protection-against-creating-too-many-instances) + - [Configuration](#configuration) + - [Build / Deploy](#build--deploy) + - [Elastic Buildkite PR Bot](#elastic-buildkite-pr-bot) + - [Overview](#overview-2) + - [Configuration](#configuration-1) + - [Build / Deploy](#build--deploy-1) + - [Infrastructure](#infrastructure) + - [Monitoring / Alerting](#monitoring--alerting) + - [Agent Image management](#agent-image-management) + - [Buildkite org-level settings management](#buildkite-org-level-settings-management) + - [IT Security Processes](#it-security-processes) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) + - [Jenkins](#jenkins) + - [Required](#required-2) + - [Scalable](#scalable-2) + - [Stable](#stable-2) + - [Updates](#updates) + - [Surfaces information intuitively](#surfaces-information-intuitively-2) + - [Pipelines](#pipelines-2) + - [Advanced Pipeline logic](#advanced-pipeline-logic-2) + - [Cloud-friendly pricing model](#cloud-friendly-pricing-model-2) + - [Public access](#public-access-2) + - [Secrets handling](#secrets-handling-2) + - [Support or Documentation](#support-or-documentation-2) + - [Scheduled Builds](#scheduled-builds-2) + - [Container support](#container-support-2) + - [Desired](#desired-2) + - [Customization](#customization-2) + - [Core functionality is first-party](#core-functionality-is-first-party-2) + - [First-class support for test results](#first-class-support-for-test-results-2) + - [GitHub Integration](#github-integration-2) + - [Other solutions](#other-solutions) + - [CircleCI](#circleci) + - [GitHub Actions](#github-actions) +- [Adoption strategy](#adoption-strategy) +- [How we teach this](#how-we-teach-this) + +# Summary + +Implement a CI system for Kibana teams that is highly scalable and stable, surfaces information in an intuitive way, and supports pipelines that are easy to understand and change. + +This table provides an overview of the conclusions made throughout the rest of this document. A lot of this is subjective, but we've tried to take an honest look at each system and feature, based on a large amount of research on and/or experience with each system, our requirements, and our preferences as a team. Your team would likely come to different conclusions based on your preferences and requirements. + +| | Jenkins | Buildkite | GitHub Actions | CircleCI | TeamCity | +| ------------------------------------ | ------- | --------- | -------------- | -------- | -------- | +| Scalable | No | Yes | No | Yes | No | +| Stable | No | Yes | No | Yes | Partial | +| Surfaces information intuitively | No | Yes | No | Yes | Yes | +| Pipelines | Yes | Yes | Yes | Yes | Partial | +| Advanced Pipeline logic | Yes | Yes | Partial | Partial | No | +| Cloud-friendly pricing model | Yes | Yes | Yes | No | No | +| Public access | Yes | Yes | Yes | Partial | Yes | +| Secrets handling | Yes | Partial | Yes | Partial | Partial | +| Support or Documentation | No | Yes | Yes | Partial | Yes | +| Scheduled Builds | Yes | Yes | Yes | Yes | Yes | +| Container support | Partial | Yes | Yes | Yes | Partial | +| | | | | | | +| Customization | No | Yes | No | No | No | +| Core functionality is first-party | No | Yes | Mostly | Yes | Mostly | +| First-class support for test results | Buggy | No | No | Yes | Yes | +| GitHub Integration | Yes | Limited | Yes | Yes | Yes | + +# Motivation + +We have lived with the scalability and stability problems of our current Jenkins infrastructure for several years. We have spent a significant amount of time designing around problems, and are limited in how we can design our pipelines. Since the company-wide effort to move to a new system has been cancelled for the foreseeable future, we are faced with either re-engineering the way we use Jenkins, or exploring other solutions and potentially managing one ourselves. + +This RFC is focused on the option of using a system other than Jenkins, and managing it ourselves (to the extent that it must be managed). If the RFC is rejected, the alternative will be to instead invest significantly into Jenkins to further stabilize and scale our usage of it. + +## Required and Desired Capabilities + +### Required + +#### Scalable + +- Able to run 100s of pipelines and 1000s of individual steps in parallel without issues. +- If scaling agents/hosts is self-managed, dynamically scaling up and down based on usage should be supported and reasonably easy to do. + +#### Stable + +- Every minute of downtime can affect 100s of developers. +- The Kibana Operations team can't have an on-call rotation, so we need to minimize our responsibilities around stability/uptime. +- For systems provided as a service, they should not have frequent outages. This is a bit hard to define. 1-2 hours of downtime, twice a month, during peak working hours, is extremely disruptive. 10 minutes of downtime once or twice a week can also be very disruptive, as builds might need to be re-triggered, etc. +- For self-hosted solutions, they should be reasonably easy to keep online and have a solution for high-availability. At a minimum, most upgrades should not require waiting for all currently running jobs to finish before deploying. +- Failures are ideally handled gracefully. For example, agents may continue running tasks correctly, once the primary service becomes available again. + +#### Surfaces information intuitively + +- Developers should be able to easily understand what happened during their builds, and find information related to failures. +- User interfaces should be functional and easy to use. +- Overview and details about failures and execution time are particularly important. + +#### Pipelines + +- Pipelines should be defined as code. +- Pipelines should be reasonably easy to understand and change. Kibana team members should be able to follow a simple guide and create new pipelines on their own. +- Changes to pipelines should generally be able to be tested in Pull Requests before being merged. + +#### Advanced Pipeline logic + +With such a large codebase and CI pipeline, we often have complex requirements around when and how certain tasks should run, and we want the ability to handle this built into the system we use. It can be very difficult and require complex solutions for fairly simple use cases when the system does not support advanced pipeline logic out of the box. + +For example, the flaky test suite runner that we currently have in Jenkins is fairly simple: run a given task (which might have a dependency) `N` number of times on `M` agents. This is very difficult to model in a system like TeamCity, which does not have dynamic dependencies. + +- Retries + - Automatic (e.g. run a test suite twice to account for flakiness) and manual (user-initiated) + - Full (e.g. a whole pipeline) and partial (e.g. a single step) +- Dynamic pipelines + - Conditional dependencies/steps + - Based on user input + - Based on external events/data (e.g. PR label) + - Based on source code or changes (e.g. only run this for .md changes) +- Metadata and Artifacts re-usable between tasks + - Metadata could be a docker image tag for a specific task, built from a previous step + +#### Cloud-friendly pricing model + +If the given system has a cost, the pricing model should be cloud-friendly and/or usage-based. + +A per-agent or per-build model based on peak usage in a month is not a good model, because our peak build times are generally short-lived (e.g. around feature freeze). + +A model based on build-minutes can also be bad, if it encourages running things in parallel on bigger machines to keep costs down. For example, running two tasks on a single 2-CPU machine with our own orchestration should not be cheaper than running two tasks on two 1-CPU machines using the system's built-in orchestration. + +#### Public access + +Kibana is a publicly-available repository with contributors from outside Elastic. CI information needs to be available publicly in some form. + +#### Secrets handling + +Good, first-class support for handling secrets is a must-have for any CI system. This support can take many forms. + +- Secrets should not need to be stored in plaintext, in a repo nor on the server. +- For systems provided as a service, it is ideal if secrets are kept mostly/entirely on our infrastructure. +- There should be protections against accidentally leaking secrets to the console. +- There should be programmatic ways to manage secrets. +- Secrets are, by nature, harder to handle. However, the easier the system makes it, the more likely people are to follow best practices. + +#### Support or Documentation + +For paid systems, both self-hosted and as a service, good support is important. If a problem specific to Elastic is causing us downtime, we expect quick and efficient support. Again, 100s of developers are potentially affected by downtime. + +For open source solutions, good documentation is especially important. If much of the operational knowledge of a system can only be gained by working with the system and/or reading the source code, it will be harder to solve problems quickly. + +#### Scheduled Builds + +We have certain pipelines (ES Snapshots) that run once daily, and `master` CI currently only runs once an hour. We need the ability to configure scheduled builds. + +#### Container support + +We have the desire to use containers to create fast, clean environments for CI stages that can also be used locally. We think that we can utilize [modern layer-caching options](https://github.com/moby/buildkit#cache), both local and remote, to optimize bootstrapping various CI stages, doing retries, etc. + +For self-hosted options, containers will allow us to utilize longer-running instances (with cached layers, git repos, etc) without worrying about polluting the build environment between builds. + +If we use containers for CI stages, when a test fails, developers can pull the image and reproduce the failure in the same environment that was used in CI. + +So, we need a solution that at least allows us to build and run our own containers. The more features that exist for managing this, the easier it will be. + +### Desired + +#### Customization + +We have very large CI pipelines which generate a lot of information (bundle sizes, performance numbers, etc). Being able to attach this information to builds, so that it lives with the builds in the CI system, is highly desirable. The alternative is building custom reports and UIs outside of the system. + +#### Core functionality is first-party + +Most core functionality that we depend on should be created and maintained by the organization maintaining the CI software. It's important for bugs to be addressed quickly, for security issues to be resolved, and for functionality to be tested before a new release of the system. In this way, there is a large amount of risk associated with relying on third-party solutions for too much core functionality. + +#### First-class support for test results + +One of the primary reasons we run CI is to run tests and make sure they pass. There are currently around 65,000 tests (unit, integration, and functional) that run in CI. Being able to see summaries, histories, and details of test execution directly on build pages is extremely useful. Flaky test identification is also very useful, as we deal with flaky tests on a daily basis. + +For example, being able to easily see that a build passed but included 5,000 tests fewer than the previous build can make something like a pipeline misconfiguration more obvious. Being able to click on a failed test and see other recent builds where the same test failed can help identify what kind of failure it is and how important it is to resolve it quickly (e.g is it failing in 75% of builds or 5% of builds?). + +For any system that doesn't have this kind of support, we will need to maintain our own solution, customize build pages to include this (if the system allows), or both. + +#### GitHub Integration + +- Ability to trigger jobs based on webhooks +- Integrate GitHub-specific information into UI, e.g. a build for a PR should link back to the PR +- Ability to set commit statuses based on job status +- Fine-grained permission handling for pull request triggering + +# Buildkite - Detailed design + +For the alternative system in this RFC, we are recommending Buildkite. The UI, API, and documentation have been a joy to work with, they provide most of our desired features and functionality, the team is responsive and knowledgeable, and the pricing model does not encourage bad practices to lower cost. + +## Overview + +[Buildkite](https://buildkite.com/home) is a CI system where the user manages and hosts their own agents, and Buildkite manages and hosts everything else (core services, APIs, UI). + +The [Buildkite features](https://buildkite.com/features) page is a great overview of the functionality offered. + +For some public instances of Buildkite in action, see: + +- [Bazel](https://buildkite.com/bazel) +- [Rails](https://buildkite.com/rails) +- [Chef](https://buildkite.com/chef-oss) + +## Required and Desired Capabilities + +How does Buildkite stack up against our required and desired capabilities? + +### Required + +#### Scalable + +Buildkite claims to support up to 10,000 connected agents "without breaking a sweat." + +We were able to connect 2,200 running agents and run a [single job with 1,800 parallel build steps](https://buildkite.com/elastic/kibana-custom/builds/8). The job ran with only about 15 seconds of total overhead (the rest of the time, the repo was being cloned, or the actual tasks were executing). We would likely never define a single job this large, but not only did it execute without any problems, the UI handles it very well. + +2,200 agents was the maximum that we were able to test because of quotas on our GCP account that could not easily be increased. + +We also created a job with 5 parallel steps, and triggered 300 parallel builds at once. The jobs executed and finished quickly, across ~1500 agents, with no issues and very little overhead. Interestingly, it seems that we were able to see the effects of our test in Buildkite's status page graphs (see below), but, from a user perspective, we were unable to notice any issues. + +![Status Graphs](../images/0018_buildkite_uptime.png) + +#### Stable + +So far, we have witnessed no stability issues in our testing. + +If Buildkite's status pages are accurate, they seem to be extremely stable, and respond quickly to issues. + +- [Buildkite Status](https://www.buildkitestatus.com/) +- [Historical Uptime](https://www.buildkitestatus.com/uptime) +- [Incident History](https://www.buildkitestatus.com/history) + +For agents, stability and availability will depend primarily on the infrastructure that we build and the availability of the cloud provider (GCP, primarily) running our agents. Since [we control our agents](#elastic-buildkite-agent-manager), we will be able to run agents across multiple zones, and possibly regions, in GCP for increased availability. + +They have a [99.95% uptime SLA](https://buildkite.com/enterprise) for Enterprise customers. + +#### Surfaces information intuitively + +The Buildkite UI is very easy to use, and works as expected. Here is some of the information surfaced for each build: + +- The overall status of the job, as well as which steps succeeded and failed. +- Logs for each individual step +- The timeline for each individual step, including how long it took Buildkite to schedule/handle the job on their end +- Artifacts uploaded by each step +- The entire agent/job configuration at the time the step executed, expressed as environment variables + +![Example Build](../images/0018_buildkite_build.png) + +Note that dependencies between steps are mostly not shown in the UI. See screenshot below for an example. There are several layers of dependencies between all of the steps in this pipeline. The only one that is shown is the final step (`Post All`), which executes after all steps beforehand are finished. There are some other strategies to help organize the steps (such as the new grouping functionality) if we need. + +![Dependencies](../images/0018_buildkite_deps.png) + +Buildkite has rich build page customization via "annotations" which will let us surface custom information. See the [customization section](#customization-1). + +#### Pipelines + +- [Buildkite pipelines](https://buildkite.com/docs/pipelines) must be defined as code. Even if you configure them through the UI, you still have to do so using yaml. +- This is subjective, but the yaml syntax for pipelines is friendly and straightforward. We feel that it will be easy for teams to create and modify pipelines with minimal instructions. +- If your pipeline is configured to use yaml stored in your repo for its definition, branches and PRs will use the version in their source by default. This means that PRs that change the pipeline can be tested as part of the PR CI. +- Top-level pipeline configurations, i.e. basically a pointer to a repo that has the real pipeline yaml in it, can be configured via the UI, API, or terraform. + +#### Advanced Pipeline logic + +Buildkite supports very advanced pipeline logic, and has support for generating dynamic pipeline definitions at runtime. + +- [Conditionals](https://buildkite.com/docs/pipelines/conditionals) +- [Dependencies](https://buildkite.com/docs/pipelines/dependencies) with lots of options, including being optional/conditional +- [Retries](https://buildkite.com/docs/pipelines/command-step#retry-attributes), both automatic and manual, including configuring retry conditions by different exit codes +- [Dynamic pipelines](https://buildkite.com/docs/pipelines/defining-steps#dynamic-pipelines) - pipelines can be generated by running a script at runtime +- [Metadata](https://buildkite.com/docs/pipelines/build-meta-data) can be set in one step, and read in other steps +- [Artifacts](https://buildkite.com/docs/pipelines/artifacts) can be uploaded from and downloaded in steps, and are visible in the UI +- [Parallelism and Concurrency](https://buildkite.com/docs/tutorials/parallel-builds) settings + +Here's an example of a dynamically-generated pipeline based on user input that runs a job `RUN_COUNT` times (from user input), across up to a maximum of 25 agents at once: + +```yaml +# pipeline.yml + +steps: + - input: 'Test Suite Runner' + fields: + - select: 'Test Suite' + key: 'test-suite' + required: true + options: + - label: 'Default CI Group 1' + value: 'default:cigroup:1' + - label: 'Default CI Group 2' + value: 'default:cigroup:2' + - text: 'Number of Runs' + key: 'run-count' + required: true + default: 75 + - wait + - command: .buildkite/scripts/flaky-test-suite-runner.sh | buildkite-agent pipeline upload + label: ':pipeline: Upload' +``` + +```bash +#!/usr/bin/env bash + +# flaky-test-suite-runner.sh + +set -euo pipefail + +TEST_SUITE="$(buildkite-agent meta-data get 'test-suite')" +export TEST_SUITE + +RUN_COUNT="$(buildkite-agent meta-data get 'run-count')" +export RUN_COUNT + +UUID="$(cat /proc/sys/kernel/random/uuid)" +export UUID + +cat << EOF +steps: + - command: | + echo 'Bootstrap' + label: Bootstrap + agents: + queue: bootstrap + key: bootstrap + - command: | + echo 'Build Default Distro' + label: Build Default Distro + agents: + queue: bootstrap + key: default-build + depends_on: bootstrap + - command: 'echo "Running $TEST_SUITE"; sleep 10;' + label: 'Run $TEST_SUITE' + agents: + queue: ci-group + parallelism: $RUN_COUNT + concurrency: 25 + concurrency_group: '$UUID' + depends_on: default-build +EOF +``` + +#### Cloud-friendly pricing model + +Buildkite is priced using a per-user model, where a user is effectively an Elastic employee triggering builds for Kibana via PR, merging code, or through the Buildkite UI. That means that the cost essentially grows with our company size. Most importantly, we don't need to make CI pipeline design decisions based on the Buildkite pricing model. + +However, since we manage our own agents, we will still pay for our compute usage, and will need to consider that cost when designing our pipelines. + +#### Public access + +Buildkite has read-only public access, configurable for each pipeline. An organization can contain a mix of both public and private pipelines. + +There are not fine-grained settings for this, and all information in the build is publicly accessible. + +#### Secrets handling + +[Managing Pipeline Secrets](https://buildkite.com/docs/pipelines/secrets) + +Because agents run on customers' infrastructure, secrets can stay completely in the customer's environment. For this reason, Buildkite doesn't provide a real mechanism for storing secrets, and instead provide recommendations for accessing secrets in pipelines in secure ways. + +There are two recommended methods for handling secrets: using a third-party secrets service like Vault or GCP's Secret Manager, or baking them into agent images and only letting certain jobs access them. Since Elastic already uses Vault, we could utilize Vault the same way we do in Jenkins today. + +Also, a new experimental feature, [redacted environment variables](https://buildkite.com/docs/pipelines/managing-log-output#redacted-environment-variables) can automatically redact the values of environment variables that match some configurable suffixes if they are accidentally written to the console. This would only redact environment variables that were set prior to execution of a build step, e.g. during the `environment` or `pre-command` hooks, and not variables that were created during execution, e.g. by accessing Vault in the middle of a build step. + +#### Support or Documentation + +[Buildkite's documentation](https://buildkite.com/docs/pipelines) is extensive and well-written, as mentioned earlier. + +Besides this, [Enterprise](https://buildkite.com/enterprise) customers get 24/7 emergency help, prioritized support, a dedicated chat channel, and guaranteed response times. They will also consult on best practices, etc. + +#### Scheduled Builds + +[Buildkite has scheduled build](https://buildkite.com/docs/pipelines/scheduled-builds) support with a cron-like syntax. Schedules are defined separately from the pipeline yaml, and can be managed via the UI, API, or terraform. + +#### Container support + +Since we will manage our own agents with Buildkite, we have full control over the container management tools we install and use. In particular, this means that we can easily use modern container tooling, such as Docker with Buildkit, and we can pre-cache layers or other data in our agent images. + +[Buildkite maintains](https://buildkite.com/docs/tutorials/docker-containerized-builds) two officially-supported plugins for making it easier to create pipelines using containers: [one for Docker](https://github.com/buildkite-plugins/docker-buildkite-plugin) and [one for Docker Compose](https://github.com/buildkite-plugins/docker-compose-buildkite-plugin). + +The Docker plugin is essentially a wrapper around `docker run` that makes it easier to define steps that run in containers, while setting various flags. It also provides some logging, and provides mechanisms for automatically propagating environment variables or mounting the workspace into the container. + +A simple, working example for running Jest tests using a container is below. The `Dockerfile` contains all dependencies for CI, and runs `yarn kbn bootstrap` so that it contains a full environment, ready to run tasks. + +```yaml +steps: + - command: | + export DOCKER_BUILDKIT=1 && \ + docker build -t gcr.io/elastic-kibana-184716/buildkite/ci/base:$BUILDKITE_COMMIT -f .ci/Dockerfile . --progress plain && \ + docker push gcr.io/elastic-kibana-184716/buildkite/ci/base:$BUILDKITE_COMMIT + - wait + - command: node scripts/jest --ci --verbose --maxWorkers=6 + label: 'Jest' + artifact_paths: target/junit/**/*.xml + plugins: + - docker#v3.8.0: + image: 'gcr.io/elastic-kibana-184716/buildkite/ci/base:$BUILDKITE_COMMIT' + propagate-environment: true + mount-checkout: false + parallelism: 2 + timeout_in_minutes: 120 +``` + +### Desired + +#### Customization + +We have very large CI pipelines which generate a lot of information (bundle sizes, performance numbers, etc). Being able to attach this information to builds, so that it lives with the builds in the CI system, is highly desirable. The alternative is building custom reports and UIs outside of the system. + +[Annotations](https://buildkite.com/docs/agent/v3/cli-annotate) provide a way to add rich, well-formatted, custom information to build pages using CommonMark Markdown. There are several built-in CSS classes for formatting and several visual styles. Images, emojis, and links can be embedded as well. Just for some examples: Metrics such as bundle sizes, links to the distro builds for that build, and screenshots for test failures could all be embedded directly into the build pages. + +The structure of logs can also be easily customized by adding [collapsible groups](https://buildkite.com/docs/pipelines/managing-log-output#collapsing-output) for log messages. + +#### Core functionality is first-party + +There's a large number of [plugins for Buildkite](https://buildkite.com/plugins), but, so far, there are only two plugins we've been considering using (one for Docker and one for test results), and they're both maintained by Buildkite. All other functionality we've assessed that we need is either built directly into Buildkite, or [we are building it](#what-we-will-build-and-manage). + +#### First-class support for test results + +Buildkite doesn't really have any built-in support specifically for handling test results. Test result reports (e.g. JUnit) can be uploaded as artifacts, and test results can be rendered on the build page using annotations. They have [a plugin](https://github.com/buildkite-plugins/junit-annotate-buildkite-plugin) for automatically annotating builds with test results from JUnit reports in a simple fashion. We would likely want to build our own annotation for this. + +This does mean that Buildkite lacks test-related features of other CI systems: tracking tests over time across build, flagging flaky tests, etc. We would likely need to ingest test results into Elasticsearch and build out Kibana dashboards/visualizations for this, or similar. + +#### GitHub Integration + +Buildkite's [GitHub Integration](https://buildkite.com/docs/integrations/github) can trigger builds based on GitHub webhooks (e.g. on commit/push for branches and PRs), and update commit statuses. Buildkite also adds basic information to build pages, such as links to commits on GitHub and links to PRs. This should cover what we need for tracked branch builds. + +However, for Pull Requests, because we have a lot of requirements around when builds should run and who can run them, we will need to [build a solution](#elastic-buildkite-pr-bot) for handling PRs ourselves. The work for this is already close to complete. + +## What we will build and manage + +### Elastic Buildkite Agent Manager + +#### Overview + +Currently, with Buildkite, the agent lifecycle is managed entirely by customers. Customers can run "static" workers that are online all of the time, or dynamically scale their agents up and down as needed. + +For AWS, Buildkite maintains an auto-scaling solution called [Elastic CI Stack for AWS](https://github.com/buildkite/elastic-ci-stack-for-aws). + +Since, we primarily need support for GCP, we built our own agent manager. It's not 100% complete, but has been working very well during our testing/evaluation of Buildkite, and can handle 1000s of agents. + +[Elastic Buildkite Agent Manager](https://github.com/brianseeders/buildkite-agent-manager) + +Features: + +- Handles many different agent configurations with one instance +- Configures long-running agents, one-time use agents, and agents that will terminate after being idle for a configured amount of time +- Configures both minimum and maximum agent limits - i.e. can ensure a certain number of agents are always online, even if no jobs currently require them +- Supports overprovisioning agents by a percentage or a fixed number +- Supports many GCE settings: zone, image/image family, machine type, disk type and size, tags, metadata, custom startup scripts +- Agent configuration is stored in a separate repo and read at runtime +- Agents are gracefully replaced (e.g. after they finish their current job) if they are running using an out-of-date agent configuration that can affect the underlying GCE instance +- Detect and remove orphaned GCP instances +- Handles 1000s of agents (tested with 2200 before we hit GCP quotas) +- Does instance creation/deletion in large, parallel batches so that demand spikes are handled quickly + +Also planned: + +- Balance creating agents across numerous GCP zones for higher availability +- Automatically gracefully replace agents if disk usage gets too high +- Scaling idle timeouts: e.g. the first agent for a configuration might have an idle timeout of 1 hour, but the 200th might be 5 minutes + +#### Design + +The agent manager is primarily concerned with ensuring that, given an agent configuration, the number of online agents for that configuration is **greater than or equal to** the desired number. Buildkite then determines how to use the agents: which jobs they should execute and when they should go offline (due to being idle, done with jobs, etc). Even when stopping agents due to having an outdated configuration, Buildkite still determines the actual time that the agent should disconnect. + +The current version of the agent manager only handles GCP-based agents, but support for other platforms could be added as well, such as AWS or Kubernetes. There's likely more complexity in managing all of the various agent images than in maintaining support in the agent manager. + +It is also designed to itself be stateless, so that it is easy to deploy and reason about. State is effectively stored in GCP and Buildkite. + +![High-Level Design](../images/0018_agent_manager.png) + +The high-level design for the agent manager is pretty straightforward. There are three primary stages during execution: + +1. Gather Current State + 1. Data and agent configuration is gathered from various sources/APIs in parallel +2. Create Plan + 1. Given the current state across the various services, a plan is created based on agent configurations, current Buildkite job queue sizes, and current GCE instances. + 2. Instances need to be created when there aren't enough online/in-progress agents of a particular configuration to satisfy the needs of its matching queue. + 3. Agents need to be stopped when the agents have been online for too long (based on their configuration) or when their configuration is out-of-date. This is a soft stop, they will terminate after finishing their current job. + 4. Instances need to be deleted if they have been stopped (which happens when their agent stops), or when they have been online past their hard stop time (based on configuration). +3. Execute Plan + 1. The different types of actions in the plan are executed in parallel. Instance creating and deleting is done in batches to handle spikes quickly. + +An error at any step, e.g. when checking current state of GCP instances, will cause the rest of the run to abort. + +Because the service gathers data about the total current state and creates a plan based on that state each run, it's reasonably resistant to errors and it's self-healing. + +##### Protection against creating too many instances + +Creating too many instances in GCP could be costly, so it is worth mentioning here. Since the agent manager itself is stateless, and only looks at the current, external state when determining an execution plan, there is the possibility of creating too many instances. + +There are two primary mechanisms to protect against this: + +One is usage of GCP quotas. Maintaining reasonable GCP quotas will ensure that we don't create too many instances in a situation where something goes catastrophically wrong during operation. It's an extra failsafe. + +The other is built into the agent manager. The agent manager checks both the number of connected agents in Buildkite for a given configuration, as well as the number of instances currently running and being created in GCP. It uses whichever number is greater as the current number of instances. + +This is a simple failsafe, but means that a large number of unnecessary instances should only be able to be created in a pretty specific scenario (keep in mind that errors will abort the current agent manager run): + +- The GCP APIs (both read and create) are returning success codes +- The GCP API for listing instances is returning partial/missing/erroneous data, with a success code +- GCP instances are successfully being created +- Created GCP instances are unable to connect to Buildkite, or Buildkite Agents API is returning partial/missing/erroneous data + +All of these things would need to be true at the same time for a large number of instances to be created. In the unlikely event that that were to happen, the GCP quotas would still be in-place. + +#### Configuration + +Here's an example configuration, which would likely reside in the `master` branch of the kibana repository. + +```js +{ + gcp: { + // Configurations at this level are defaults for all configurations defined under `agents` + project: 'elastic-kibana-184716', + zone: 'us-central1-b', + serviceAccount: 'elastic-buildkite-agent@elastic-kibana-184716.iam.gserviceaccount.com', + agents: [ + { + queue: 'default', + name: 'kibana-buildkite', + overprovision: 0, // percentage or flat number + minimumAgents: 1, + maximumAgents: 500, + gracefulStopAfterSecs: 60 * 60 * 6, + hardStopAfterSecs: 60 * 60 * 9, + idleTimeoutSecs: 60 * 60, + exitAfterOneJob: false, + imageFamily: 'kibana-bk-dev-agents', + machineType: 'n2-standard-1', + diskType: 'pd-ssd', + diskSizeGb: 75 + }, + { + // ... + }, + } +} +``` + +#### Build / Deploy + +Currently, the agent manager is built and deployed using [Google Cloud Build](https://cloud.google.com/build). It is deployed to and hosted using [GKE Auto-Pilot](https://cloud.google.com/blog/products/containers-kubernetes/introducing-gke-autopilot) (Kubernetes). GKE was used, rather than Cloud Run, primarily because the agent manager runs continuously (with a 30sec pause between executions) whereas Cloud Run is for services that respond to HTTP requests. + +It uses [Google Secret Manager](https://cloud.google.com/secret-manager) for storing/retrieving tokens for accessing Buildkite. It uses a GCP service account and [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) to manage GCP resources. + +### Elastic Buildkite PR Bot + +#### Overview + +For TeamCity, we built a bot that was going to handle webhooks from GitHub and trigger builds for PRs based on configuration, user permissions, etc. Since we will not be moving to TeamCity, we've repurposed this bot for Buildkite, since Buildkite does not support all of our requirements around triggering builds for PRs out-of-the-box. The bot supports everything we currently use in Jenkins, and has some additional features as well. + +[Elastic Buildkite PR Bot](https://github.com/elastic/buildkite-pr-bot) + +Features supported by the bot: + +- Triggering builds on commit / when the PR is opened +- Triggering builds on comment +- Permissions for who can trigger builds based on: Elastic org membership, write and/or admin access to the repo, or user present in an allowed list +- Limit builds to PRs targeting a specific branch +- Custom regex for trigger comment, e.g. "buildkite test this" +- Triggering builds based on labels +- Setting labels, comment body, and other PR info as env vars on triggered build +- Skip triggering build if a customizable label is present +- Option to set commit status on trigger +- Capture custom arguments from comment text using capture groups and forward them to the triggered build + +#### Configuration + +The configuration is stored in a `json` file (default: `.ci/pull-requests.json`) in the repo for which pull requests will be monitored. Multiple branches in the repo can store different configurations, or one configuration (e.g. in `master`) can cover the entire repo. + +Example configuration: + +```json +{ + "jobs": [ + { + "repoOwner": "elastic", + "repoName": "kibana", + "pipelineSlug": "kibana", + + "enabled": true, + "target_branch": "master", + "allow_org_users": true, + "allowed_repo_permissions": ["admin", "write"], + "allowed_list": ["renovate[bot]"], + "set_commit_status": true, + "commit_status_context": "kibana-buildkite", + "trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))|^retest$" + } + ] +} +``` + +Github Webhooks must also be configured to send events to the deployed bot. + +#### Build / Deploy + +Currently, the bot is built and deployed using [Google Cloud Build](https://cloud.google.com/build). It is deployed to and hosted on [Google Cloud Run](https://cloud.google.com/run). It uses [Google Secret Manager](https://cloud.google.com/secret-manager) for storing/retrieving tokens for accessing GitHub and Buildkite. + +[Build/deploy configuration](https://github.com/elastic/buildkite-pr-bot/blob/main/cloudbuild.yaml) + +### Infrastructure + +We will need to maintain our infrastructure related to Buildkite, primarily ephemeral agents. To start, it will mean supporting infrastructure in GCP, but could later mean AWS as well. + +- Separate GCP project for CI resources +- Hosting for bots/services we maintain, such as the Agent Manager (GKE Auto-Pilot) and GitHub PR bot (Cloud Run) +- Google Storage Buckets for CI artifacts +- Networking (security, we may also need Cloud NAT) +- IAM and Security +- Agent images + +We are already using Terraform to manage most resources related to Buildkite, and will continue to do so. + +### Monitoring / Alerting + +We will need to set up and maintain monitoring and alerting for our GCP infrastructure, as well as Buildkite metrics. + +Some examples: + +GCP + +- Number of instances by type +- Age of instances +- Resource Quotas + +Buildkite + +- Agent queues +- Job wait times +- Build status + +### Agent Image management + +We will need to maintain images used to create GCP instances for our Buildkite agents. These images would need to be built on a regular basis (daily, or possibly more often). + +We could likely maintain a single linux-based image to cover all of our current CI needs. However, in the future, if we need to maintain many images across different operating systems and architectures, this is likely to become the most complex part of the CI system that we would need to maintain. Every operating system and architecture we need to support adds another group of required images, with unique dependencies and configuration automation. + +Another thing to note: Just because we need to run something on a specific OS or architecture, it doesn't necessarily mean we need to maintain an agent image for it. For example, we might use something like Vagrant to create a separate VM, using the default, cloud-provided images, that we run something on (e.g. for testing system packages), rather than running it on the same machine as the agent. In this case, we would potentially only be managing a small number of images, or even a single image. + +Also, we always have the option of running a small number of jobs using Jenkins, if we need to do so to target additional OSes and architectures. + +For our testing, we have a single GCP image, [built using Packer](https://github.com/elastic/kibana/tree/kb-bk/.buildkite/agents/packer), with the Buildkite agent installed and all of our dependencies. + +Summary of Responsibilities + +- An automated process for creating new images, at least daily, running automated smoke tests against them, and promoting them +- Delete old images when creating new ones +- Ability to roll back images easily and/or pin specific image versions +- Manage dependencies, failures, updates, etc across all supported OSes and architectures, on a regular basis + +### Buildkite org-level settings management + +There are a few settings outside of pipelines that we will need to manage. + +- Top-level pipelines and their settings +- Pipeline schedules / scheduled jobs +- Public visibility of pipelines +- Teams and Permissions +- Single Sign On settings + +Most of the content for our pipelines will be stored in repositories as YAML. However, a job still must exist in Buildkite that points to that repo and that YAML. For managing those top-level configurations, an official [Terraform provider](https://registry.terraform.io/providers/buildkite/buildkite/latest/docs/resources/pipeline) exists, which we will likely take advantage of. + +Pipeline schedules can also be managed using the Terraform provider. + +Teams can also be managed using Terraform, but it's unlikely we will need to use Teams. + +For everything else, we will likely start off using UI and build automation (or contribute to the Terraform provider) where we see fit. Most of the other settings are easy to configure, and unlikely to change. + +### IT Security Processes + +There will likely be numerous IT Security processes we will need to follow, since we will be managing infrastructure. This could include regular audits, specific software and configurations that must be baked into our agents, documentation procedures, or other conditions that we will need to satisfy. There is risk here, as the processes and workload are currently unknown to us. + +# Drawbacks + +The biggest drawback to doing this is that we will be duplicating a large amount of work and providing/maintaining a service that is already provided to us by another team at Elastic. Jenkins is already provided to us, and there is automation for creating Jenkins worker images and managing worker instances in both AWS and GCP, and IT Security policies are already being handled for all of this. It is hard to predict what the extra workload will be for the Kibana Operations team if we move our CI processes to Buildkite, but we know we will have to maintain all of the things listed under [What we will build and manage](#what-we-will-build-and-manage). + +Some other drawbacks: + +- CI Pipelines and other jobs built in Jenkins will need to be re-built, which includes building support for things like CI Stats, Slack notifications, GitHub PR comments, etc. +- Developers will need to learn a new system. +- The service is an additional cost to the company. +- There is a lot of Jenkins knowledge throughout the company, but likely little Buildkite knowledge. + +# Alternatives + +## Jenkins + +We are not happy with the experience provided by our instance of Jenkins and our current pipelines. If we stick with Jenkins, we will need to invest a likely significant amount of time in improving the experience and making our pipelines scale given the limitations we face. + +### Required + +#### Scalable + +Our current Jenkins instance only allows for 300-400 connected agents, before effectively going offline. We have struggled with this issue for several years, and completely redesigned our pipelines around this limitation. The resulting design, which involves running 20+ tasks in parallel on single, large machines, and managing all of the concurrency ourselves, is complicated and problematic. + +Other teams at Elastic, especially over the last few months, have been experiencing this same limitation with their Jenkins instances as well. The team that manages Jenkins at Elastic is well aware of this issue, and is actively investigating. It is currently unknown whether or not it is a solvable problem (without sharding) or a limitation of Jenkins. + +#### Stable + +Firstly, Jenkins was not designed for high availability. If the primary/controller goes offline, CI is offline. + +The two biggest sources of stability issues for us are currently related to scaling (see above) and updates. + +##### Updates + +The typical update process for Jenkins looks like this: + +- Put Jenkins into shutdown mode, which stops any new builds from starting +- Wait for all currently-running jobs to finish +- Shutdown Jenkins +- Do the update +- Start Jenkins + +For us, shutdown mode also means that `gobld` stops creating new agents for our jobs. This means that many running jobs will never finish executing while shutdown mode is active. + +So, for us, the typical update process is: + +- Put Jenkins into shutdown mode, which stops any new builds from starting, and many from finishing +- Hard kill all of our currently running jobs +- Shutdown Jenkins +- Do the update +- Start Jenkins +- A human manually restarts CI for all PRs that were running before the update + +This is pretty disruptive for us, as developers have to wait several hours longer before merging or seeing the status of their PRs, plus there is manual work that must be done to restart CI. If we stay with Jenkins, we'll need to fix this process, and likely build some automation for it. + +#### Surfaces information intuitively + +Our pipelines are very complex, mainly because of the issues mentioned above related to designing around scaling issues, and none of the UIs in Jenkins work well for us. + +The [Stage View](https://kibana-ci.elastic.co/job/elastic+kibana+pipeline-pull-request) only works for very simple pipelines. Even if we were able to re-design our pipelines to populate this page better, there are just too many stages to display in this manner. + +[Blue Ocean](https://kibana-ci.elastic.co/blue/organizations/jenkins/elastic%2Bkibana%2Bpipeline-pull-request/activity), which is intended to be the modern UI for Jenkins, doesn't work at all for our pipelines. We have nested parallel stages in our pipelines, which [are not supported](https://issues.jenkins.io/browse/JENKINS-54010). + +[Pipeline Steps](https://kibana-ci.elastic.co/job/elastic+kibana+pipeline-pull-request/) (Choose a build -> Pipeline Steps) shows information fairly accurately (sometimes logs/errors are not attached to any steps, and do not show), but is very difficult to read. There are entire pages of largely irrelevant information (Setting environment variables, starting a `try` block, etc), which is difficult to read through, especially developers who don't interact with Jenkins every day. + +![Pipeline Steps](../images/0018_jenkins_pipeline_steps.png) + +We push a lot of information to GitHub and Slack, and have even built custom UIs, to try to minimize how much people need to interact directly with Jenkins. In particular, when things go wrong, it is very difficult to investigate using the Jenkins UI. + +#### Pipelines + +Jenkins supports pipeline-as-code through [Pipelines](https://www.jenkins.io/doc/book/pipeline), which we currently use. + +Pros: + +- Overall pretty powerful, pipelines execute Groovy code at runtime, so pipelines can do a lot and can be pretty complex, if you're willing to write the code +- Pipeline changes can be tested in PRs +- Shared Libraries allow shared code to be used across pipelines easily + +Cons: + +- The sandbox is pretty difficult to work with. There's a [hard-coded list](https://github.com/jenkinsci/script-security-plugin/tree/e99ba9cffb0502868b05d19ef5cd205ca7e0e5bd/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists) of allowed methods for pipelines. Other methods must be approved separately, or put in a separate shared repository that runs trusted code. +- Pipeline code is serialized by Jenkins, and the serialization process leads to a lot of issues that are difficult to debug and reason about. See [JENKINS-44924](https://issues.jenkins.io/browse/JENKINS-44924) - `List.sort()` doesn't work and silently returns `-1` instead of a list +- Reasonably complex pipelines are difficult to view in the UI ([see above](#surfaces-information-intuitively-2)) +- Using Pipelines to manage certain configurations (such as Build Parameters) requires running an outdated job once and letting it fail to update it +- Jobs that reference a pipeline have to be managed separately. Only third-party tools exist for managing these jobs as code (JJB and Job DSL). +- Very difficult to test code without running it live in Jenkins + +#### Advanced Pipeline logic + +See above section. Jenkins supports very advanced pipeline logic using scripted pipelines and Groovy. + +#### Cloud-friendly pricing model + +Given that Jenkins is open-source, we pay only for infrastructure and people to manage it. + +#### Public access + +- Fine-grained authorization settings +- Anonymous user access +- Per-job authorization, so some jobs can be private + +#### Secrets handling + +- Supports [Credentials](https://www.jenkins.io/doc/book/using/using-credentials/), which are stored encrypted on disk and have authorization settings + - Credentials are difficult to manage in an automated way +- Pipeline support for accessing credentials +- Credentials masked in log output +- Support for masking custom values in log output + +#### Support or Documentation + +Documentation for Jenkins is notoriously fragmented. All major functionality is provided in plugins, and documentation is spread out across the Jenkins Handbook, the CloudBees website, JIRA issues, wikis, GitHub repos, JavaDoc pages. Many plugins have poor documentation, and source code often has to be read to understand how to configure something. + +CloudBees offers paid support, but we're not familiar with it at this time. + +#### Scheduled Builds + +Jenkins supports scheduled builds via a Cron-like syntax, and can spread scheduled jobs out. For example, if many jobs are scheduled to run every day at midnight, a syntax is available that will automatically spread the triggered jobs evenly out across the midnight hour. + +#### Container support + +Jenkins has support for using Docker to [run containers for specific stages in a Pipeline](https://www.jenkins.io/doc/book/pipeline/docker/). It is effectively a wrapper around `docker run`. There are few conveniences, and figuring out how to do things like mount the workspace into the container is left up to the user. There are also gotchas that are not well-documented, such as the fact that the user running inside the container will be automatically changed using `-u`, which can cause issues. + +Though we have control over the agents running our jobs at Elastic, and thus all of the container-related tooling, it is not currently easy for the Operations team to manage our container tooling. We are mostly dependent on another team to do this for us. + +### Desired + +#### Customization + +The only way to customize information added to build pages is through custom plugins. [Creating and maintaining plugins for Jenkins](https://www.jenkins.io/doc/developer/plugin-development/) is a fairly significant investment, and we do not currently have a good way to manage plugins for Jenkins instances at Elastic. It's a pretty involved process that, at the moment, has to be done by another team. + +Given that, we feel we would be able to build a higher-quality experience in less time by creating custom applications separate from Jenkins, which we have actually [done in the past](https://ci.kibana.dev/es-snapshots). + +#### Core functionality is first-party + +Jenkins is very modular, and almost all Jenkins functionality is provided by plugins. + +It's difficult to understand which plugins are required to support which base features. For example, Pipelines support is provided by a group of many plugins, and many of them have outdated names ([Pipeline: Nodes and Processes](https://github.com/jenkinsci/workflow-durable-task-step-plugin) is actually a plugin called `workflow-durable-task-step-plugin`). + +Many plugins are maintained by CloudBees employees, but it can be very difficult to determine which ones are, without knowing the names of CloudBees employees. All Jenkins community/third-party plugins reside under the `jenkinsci` organization in GitHub, which makes finding "official" ones difficult. Given the open source nature of the Jenkins ecosystem and the way that development is handled by Cloudbees, it might be incorrect to say that any plugins outside of the Cloudbees plugins (for the Cloudbees Jenkins distribution) are "first-party". + +#### First-class support for test results + +It's a bit buggy at times (for example, if you run the same test multiple times, you have to load pages in a specific order to see the correct results in the UI), but Jenkins does have support for ingesting and displaying test results, including graphs that show changes over time. We use this feature to ingest test results from JUnit files produced by unit tests, integration tests, and end-to-end/functional tests. + +#### GitHub Integration + +Jenkins has rich support for GitHub spread across many different plugins. It can trigger builds in response to webhook payloads, automatically create jobs for repositories in an organization, has support for self-hosted GitHub, and has many settings for triggering pull requests. + +It's worth mentioning, however, that we've had and continue to have many issues with these integrations. For example, the GitHub Pull Request Builder plugin, which currently provides PR triggering for us and other teams, has been the source of several issues at Elastic. It's had performance issues, triggers builds erroneously, and has been mostly unmaintained for several years. + +## Other solutions + +### CircleCI + +CircleCI is a mature, widely-used option that is scalable and fulfills a lot of our requirements. We felt that we could create a good CI experience with this solution, but it had several disadvantages for us compared to Buildkite: + +- The pricing model for self-hosted runners felt punishing for breaking CI into smaller tasks +- Public access to build pages is gated behind a login, and gives CircleCI access to your private repos by default +- There are no customization options for adding information to build pages +- Options for advanced pipeline logic are limited compared to other solutions + +### GitHub Actions + +GitHub Actions is an interesting option, but it didn't pass our initial consideration round for one main reason: scalability. + +To ensure we're able to run the number of parallel tasks that we need to run, we'll have to use self-hosted runners. Self-hosted runners aren't subject to concurrency limits. However, managing auto-scaling runners seems to be pretty complex at the moment, and GitHub doesn't seem to have any official guidance on how to do it. + +Also, even with self-hosted runners, there is a 1,000 API request per hour hard limit, though it does not specify which APIs. Assuming even that 1 parallel step in a job is one API request, given the large number of small tasks that we'd like to split our CI into, we will likely hit this limit pretty quickly. + +# Adoption strategy + +We have already done a lot of the required legwork to begin building and running pipelines in Buildkite, including getting approval from various business groups inside Elastic. After all business groups have signed off, and a deal has been signed with Buildkite, we can begin adopting Buildkite. A rough plan outline is below. It's not meant to be a full migration plan. + +- Build minimal supporting services, automation, and pipelines to migrate a low-risk job from Jenkins to Buildkite (e.g. "Baseline" CI for tracked branches) + - The following will need to exist (some of which has already been built) + - New GCP project for infrastructure, with current implementations migrated + - Agent Manager + - Agent image build/promote + - Slack notifications for failures (possibly utilize Buildkite's built-in solution) + - The Buildkite pipeline and supporting code + - Run the job in parallel with Jenkins until we have confidence that it's working well + - Turn off the Jenkins version +- Build, test, migrate the next low-risk pipelines: ES Snapshot and/or Flaky Test Suite Runner +- Build, test, migrate tracked branch pipelines +- Build, test, migrate PR pipelines + - Will additionally need PR comment support + - PR pipelines are the most disruptive if there are problems, so we should have a high level of confidence before migrating + +# How we teach this + +The primary way that developers interact with Jenkins/CI today is through pull requests. Since we push a lot of information to pull requests via comments, developers mostly only need to interact with Jenkins when something goes wrong. + +The Buildkite UI is simple and intuitive enough that, even without documentation, there would likely be a pretty small learning curve to navigating the build page UI that will be linked from PR comments. That's not to say we're not going to provide documentation, we just think it would be easy even without it! + +We would also like to provide simple documentation that will guide developers through setting up new pipelines without our help. Getting a new job up and running with our current Jenkins setup is a bit complicated for someone who hasn't done it before, and there isn't good documentation for it. We'd like to change that if we move to Buildkite. + +To teach and inform, we will likely do some subset of these things: + +- Documentation around new CI pipelines in Buildkite +- Documentation on how to handle PR failures using Buildkite +- Documentation on the new infrastructure, supporting services, etc. +- Zoom sessions with walkthrough and Q&A +- E-mail announcement with links to documentation +- Temporarily add an extra message to PR comments, stating the change and adding links to relevant documentation diff --git a/rfcs/text/0018_timeslider.md b/rfcs/text/0018_timeslider.md new file mode 100644 index 000000000000000..aa8e7263260d210 --- /dev/null +++ b/rfcs/text/0018_timeslider.md @@ -0,0 +1,217 @@ +- Start Date: 2020-04-26 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +--- +- [1. Summary](#1-summary) +- [2. Detailed design](#2-detailed-design) +- [3. Unresolved questions](#3-unresolved-questions) + +# 1. Summary + +A timeslider is a UI component that allows users to intuitively navigate through time-based data. + +This RFC proposes adding a timeslider control to the Maps application. + +It proposes a two phased roll-out in the Maps application. The [design proposal](#2-detailed-design) focuses on the first phase. + +Since the timeslider UI is relevant for other Kibana apps, the implementation should be portable. We propose to implement the control as a React-component +without implicit dependencies on Kibana-state or Maps-state. + +The RFC details the integration of this control in Maps. It includes specific consideration to how timeslider affects data-refresh in the context of Maps. + +This RFC also outlines a possible integration of this Timeslider-React component with an Embeddable, and the introduction of a new piece of embeddable-state `Timeslice`. + +This RFC does not address how this component should _behave_ in apps other than the Maps-app. + +# 2. Detailed design + +Below outlines: +- the two delivery phases intended for Kibana Maps +- outline of the Timeslider UI component implementation (phase 1) +- outline of the integration in Maps of the Timeslider UI component (phase 1) + +## 2.1 Design phases overview + + + +### 2.1.1 Time-range selection and stepped navigation + +A first phase includes arbitrary time-range selection and stepped navigation. + +![Timeslider version 1](../images/timeslider/v1.png) + +This is the focus of this RFC. + +Check [https://github.com/elastic/kibana/pull/96791](https://github.com/elastic/kibana/pull/96791) for a POC implementation. + +### 2.2.2 Data distribution preview with histogram and playback + +A second phase adds a date histogram showing the preview of the data. + +![Timeslider version 2](../images/timeslider/v2.png) + +Details on this phase 2 are beyond the scope of this RFC. + +## 2.2 The timeslider UI React-component (phase 1) + +This focuses on Phase 1. Phase 2, with date histogram preview and auto-playback is out of scope for now. + +### 2.2.1 Interface of the React-component + +The core timeslider-UI is a React-component. + +The component has no implicit dependencies on any Kibana-state or Maps-store state. + +Its interface is fully defined by its `props`-contract. + +``` +export type Timeslice = { + from: number; // epoch timestamp + to: number; // epoch timestamp +}; + +export interface TimesliderProps { + onTimesliceChanged: (timeslice: Timeslice) => void; + timerange: TimeRange; // TimeRange The start and end time of the entire time-range. TimeRange is defined in `data/common` + timeslice?: Timeslice; // The currently displayed timeslice. Needs to be set after onTimesliceChange to be reflected back in UI. If ommitted, the control selects the first timeslice. +} +``` + +`timeslice` is clamped to the bounds of `timeRange`. + +Any change to `timeslice`, either by dragging the handles of the timeslider, or pressing the back or forward buttons, calls the `onTimesliceChanged` handler. + +Since the initial use is inside Maps, the initial location of this React-component is inside the Maps plugin, `x-pack/plugins/maps/public/timeslider`. + +Nonetheless, this UI-component should be easily "cut-and-pastable" to another location. + +### 2.2.2 Internals + +The timeslider automatically subdivides the timerange with equal breaks that are heuristically determined. + +It assigns about 6-10 breaks within the `timerange`, snaps the "ticks" to a natural "pretty date" using `calcAutoIntervalNear` from `data/common`. + +For example; +- a `timerange` of 8.5 days, it would assign a 8 day-steps, plus some padding on either end, depending on the entire `timerange`. +- a `timerange` of 6.9 years would snap to year-long step, plus some padding on either end, depending on the entire `timerange`. + +The slider itself is a ``. + +### 2.2.2 End-user behavior + +- the user can manipulate the `timeslice`-double ranged slider to any arbitrary range within `timerange`. +- the user can press the forward and back buttons for a stepped navigation. The range of the current time-slice is preserved when there is room for the `timeslice` within the `timerange`. + - when the user has _not modified the width_ of the `timeslice`, using the buttons means stepping through the pre-determined ticks (e.g. by year, by day, ...) + - when the user has _already modified the width_ of the `timeslice`, it means stepping through the `timerange`, with a stride of the width of the `timeslice`. +- the `timeslice` "snaps" to the beginning or end (depending on direction) of the `timerange`. In practice, this means the `timeslice` will collaps or reduce in width. + +## 2.3 Maps integration of the timeslider React component + +This control will be integrated in the Maps-UI. + +Maps is Redux-based, so `timeslice` selection and activation/de-activation all propagates to the Redux-store. + +#### 2.3.1 Position in the UI + +The timeslider control is enabled/disabled by the timeslider on/off toggle-button in the main toolbar. + +![Timeslider version 1](../images/timeslider/toolbar.png) + + +#### 2.3.2 Layer interactions + + +Enabling the Timeslider will automatically retrigger refresh of _all_ time-based layers to the currently selected `timeslice`. + +The Layer-TOC will indicate which layer is currently "time-filtered" by the timeslider. + +On a layer-per-layer basis, users will be able to explicitly opt-out if they should be governed by the timerange or not. This is an existing toggle in Maps already. +This is relevant for having users add contextual layers that should _not_ depend on the time. + + +#### 2.3.3 Omitting timeslider on a dashboard + +Maps will not display the timeslider-activation button on Maps that are embedded in a Dashboard. + +We believe that a Timeslider-embeddable would be a better vehicle to bring timeslider-functionality do Dashboards. See also the [last section](#3-unresolved-questions). + +#### 2.3.3 Data-fetch considerations + +--- +**NOTE** + +The below section is very Maps-specific, although similar challenges would be present in other applications as well. + +Some of these considerations will not generalize to all of Kibana. + +The main ways that Maps distinguishes in data-fetch from other use-cases: + - the majority of the data-fetching for layers in Maps depends on the scale and extent. Ie. different data is requested based on the current zoom-level and current-extent of the Map. So for example, even if two views share the same time, query and filter-state, if their extent and/or scale is different, their requests to ES will be different. + - for some layer-types, Maps will fetch individual documents, rather than the result of an aggregation. + +--- + +Data-fetch for timeslider should be responsive and smooth. A user dragging the slider should have an immediate visual result on the map. + +In addition, we expect the timeslider will be used a lot for "comparisons". For example, imagine a user stepping back&forth between timeslices. + +For this reason, apps using a timeslider (such as Maps) ideally: +- pre-fetch data when possible +- cache data when possible + +For Maps specifically, when introducing timeslider, layers will therefore need to implement time-based data fetch based on _two_ pieces of state +- the entire `timerange` (aka. the global Kibana timerange) +- the selected `timeslice` (aka. the `timeslice` chosen by the user using the UI-component) + +##### 2.3.3.1 Pre-fetching individual documents and masking of data + +ES-document layers (which display individual documents) can prefetch all documents within the entire `timerange`, when the total number of docs is below some threshold. In the context of Maps, this threshold is the default index-search-size of the index. + +Maps can then just mask data on the map based on some filter-expression. The evaluation of this filter-expression is done by mapbox-gl is fast because it occurs on the GPU. There is immediate visual feedback to the user as they manipulate the timeslider, because it does not require a roundtrip to update the data. + +##### 2.3.3.2 Caching of aggregated searches + +Aggregated data can be cached on the client, so toggling between timeslices can avoid a round-trip data-fetch. +The main technique here is for layers to use `.mvt`-data format to request data. Tiled-based data can be cached client-side + +We do _not_ propose _pre-fetching_ of aggregated data in this initial phase of the Maps timeslider effort. There is a couple reasons: +- Based on the intended user-interactions for timeslider, because a user can flexibly select a `timeslice` of arbitrary widths, it would be really hard to determine how to determine which timeslices to aggregate up front. +- Maps already strains the maximum bucket sizes it can retrieve from Elasticsearch. Cluster/grid-layers often push up to 10k or more buckets, and terms-aggregations for choropleth maps also is going up to 10k buckets. Prefetching this for timeslices (e.g. say x10 timeslices) would easily exceed the default bucket limit sizes of Elasticsearch. + + +##### 2.3.3.3 Decouple data-fetch from UI-effort + +Apart from refactoring the data-fetch for layers to now use two pieces of time-based state, the implementation will decouple any data-fetch considerations from the actual timeslider-UI work. + +The idea is that dial in data-fetch optimizations can be dialed into Maps in a parallel work-thread, not necessarily dependent on any changes or additions to the UI. +Any optimizations would not only affect timeslider users, but support all interaction patterns that require smooth data-updates (e.g. panning back&forth to two locations, toggling back&forth between two filters, ...) + +The main effort to support efficient data-fetch in a maps-context is to use `.mvt` as the default data format ([https://github.com/elastic/kibana/issues/79868](https://github.com/elastic/kibana/issues/79868)). This is a stack-wide effort in collaboration with the Elasticsearch team ([https://github.com/elastic/elasticsearch/issues/58696](https://github.com/elastic/elasticsearch/issues/58696), which will add `.mvt` as a core response format to Elasticsearch. + +Growing the use of `mvt`([https://docs.mapbox.com/vector-tiles/reference/](https://docs.mapbox.com/vector-tiles/reference/)) in Maps will help with both pre-fetching and client-side caching: +- `mvt` is a binary format which allows more data to be packed inside, as compared to Json. Multiple tiles are patched together, so this introduces a form of parallelization as well. Due to growing the amount of data inside a single tile, and due to the parallelization, Maps has a pathway to increase the number of features that can be time-filtered. +- Because vector tiles have fixed extents and scales (defined by a `{x}/{y}/{scale}`-tuple), this type of data-fetching allows tiles to be cached on the client. This cache can be the implicit browser disc-cache, or the transient in-mem cache of mapbox-gl. Using mvt thus provides a pathway for fast toggling back&forth between timeslices, without round-trips to fetch data. + + +##### 2.3.3.4 timeslider and async search + +It is unclear on what the practical uses for async-search would be in the context of a timeslider-control in Maps. + +Timeslider is a highly interactive control that require immediate visual feedback. We also do not intend to activate timeslider in Maps on a Dashboard (see above). + +End-users who need to view a dashboard with a long-running background search will need to manipulate the _global Kibana time picker_ to select the time-range, and will not be able to use the timeslider to do so. + +# 3. Unresolved questions + +## Making Timeslider a Kibana Embeddable + +This below is a forward looking section. It is a proposal of how the Timeslider-UI can be exposed as an Embeddable, when that time should come. + +We expect a few steps: +- This would require the extraction of the timeslider React-component out of Maps into a separate plugin. As outlined above, this migration should be fairly straightforward, a "cut and paste". +- It would require the creation of a `TimesliderEmbeddable` which wraps this UI-component. +- It would also require the introduction of a new piece of embeddable-state, `Timeslice`, which can be controlled by the `TimesliderEmbeddable`. +We believe it is important to keep `timeslice` and `timerange` separate, as individual apps and other embeddables will have different mechanism to efficiently fetch data and respond to changes in `timeslice` and/or `timerange`. + +Having timeslider as a core Embeddable likely provides a better pathway to integrate timeslider-functionality in Dashboards or apps other than Maps. + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 581d614c9a371c5..b2eec43cc5ad779 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -184,7 +184,7 @@ export class DocLinksService { remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, - transportSettings: `${ELASTICSEARCH_DOCS}modules-transport.html`, + transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, }, @@ -319,6 +319,7 @@ export class DocLinksService { createSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, createRoleMapping: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html`, createRoleMappingTemplates: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html#_role_templates`, + createRollupJobsRequest: `${ELASTICSEARCH_DOCS}rollup-put-job.html#rollup-put-job-api-request-body`, createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, @@ -346,7 +347,7 @@ export class DocLinksService { snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, }, snapshotRestore: { - guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, + guide: `${KIBANA_DOCS}snapshot-repositories.html`, changeIndexSettings: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#change-index-settings-during-restore`, createSnapshot: `${ELASTICSEARCH_DOCS}snapshots-take-snapshot.html`, registerSharedFileSystem: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-filesystem-repository`, @@ -544,6 +545,7 @@ export interface DocLinksStart { createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; + createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 17ba37d075b782b..7d2a58508475837 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -144,6 +144,8 @@ export type { SavedObjectsImportSimpleWarning, SavedObjectsImportActionRequiredWarning, SavedObjectsImportWarning, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from './saved_objects'; export { HttpFetchError } from './http'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0523c523baf6fee..129a7e565394f5a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -629,6 +629,7 @@ export interface DocLinksStart { createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; + createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; @@ -1172,6 +1173,20 @@ export interface SavedObjectReference { type: string; } +// @public +export interface SavedObjectReferenceWithContext { + id: string; + inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; + isMissing?: boolean; + spaces: string[]; + spacesWithMatchingAliases?: string[]; + type: string; +} + // @public (undocumented) export interface SavedObjectsBaseOptions { namespace?: string; @@ -1239,6 +1254,12 @@ export class SavedObjectsClient { // @public export type SavedObjectsClientContract = PublicMethodsOf; +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + // (undocumented) + objects: SavedObjectReferenceWithContext[]; +} + // @public (undocumented) export interface SavedObjectsCreateOptions { coreMigrationVersion?: string; diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index e8aef5037684176..cd75bc16f83622a 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -39,6 +39,8 @@ export type { SavedObjectsImportSimpleWarning, SavedObjectsImportActionRequiredWarning, SavedObjectsImportWarning, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from '../../server/types'; export type { diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 2e77374e3068ab8..0722bbe0a71adc9 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -6,29 +6,26 @@ * Side Public License, v 1. */ -import { has, get } from 'lodash'; import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config'; const configPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(process.env, 'CONFIG_PATH')) { + if (process.env?.CONFIG_PATH) { addDeprecation({ message: `Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder`, }); } - return settings; }; const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(process.env, 'DATA_PATH')) { + if (process.env?.DATA_PATH) { addDeprecation({ message: `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`, }); } - return settings; }; const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { + if (settings.server?.basePath && !settings.server?.rewriteBasePath) { addDeprecation({ message: 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + @@ -37,20 +34,19 @@ const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'current behavior and silence this warning.', }); } - return settings; }; const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - const corsSettings = get(settings, 'server.cors'); - if (typeof get(settings, 'server.cors') === 'boolean') { + const corsSettings = settings.server?.cors; + if (typeof corsSettings === 'boolean') { addDeprecation({ message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"', }); - settings.server.cors = { - enabled: corsSettings, + + return { + set: [{ path: 'server.cors', value: { enabled: corsSettings } }], }; } - return settings; }; const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { @@ -59,7 +55,7 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); const SELF_STRING = `'self'`; - const rules: string[] = get(settings, 'csp.rules'); + const rules: string[] = settings.csp?.rules; if (rules) { const parsed = new Map( rules.map((ruleStr) => { @@ -68,34 +64,39 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati }) ); - settings.csp.rules = [...parsed].map(([policy, sourceList]) => { - if (sourceList.find((source) => source.includes(NONCE_STRING))) { - addDeprecation({ - message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, - }); - sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING)); + return { + set: [ + { + path: 'csp.rules', + value: [...parsed].map(([policy, sourceList]) => { + if (sourceList.find((source) => source.includes(NONCE_STRING))) { + addDeprecation({ + message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, + }); + sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING)); - // Add 'self' if not present - if (!sourceList.find((source) => source.includes(SELF_STRING))) { - sourceList.push(SELF_STRING); - } - } + // Add 'self' if not present + if (!sourceList.find((source) => source.includes(SELF_STRING))) { + sourceList.push(SELF_STRING); + } + } - if ( - SELF_POLICIES.includes(policy) && - !sourceList.find((source) => source.includes(SELF_STRING)) - ) { - addDeprecation({ - message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, - }); - sourceList.push(SELF_STRING); - } + if ( + SELF_POLICIES.includes(policy) && + !sourceList.find((source) => source.includes(SELF_STRING)) + ) { + addDeprecation({ + message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, + }); + sourceList.push(SELF_STRING); + } - return `${policy} ${sourceList.join(' ')}`.trim(); - }); + return `${policy} ${sourceList.join(' ')}`.trim(); + }), + }, + ], + }; } - - return settings; }; const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( @@ -103,7 +104,7 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( fromPath, addDeprecation ) => { - if (has(settings, 'map.manifestServiceUrl')) { + if (settings.map?.manifestServiceUrl) { addDeprecation({ message: 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + @@ -112,11 +113,10 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( 'modified for use in production environments.', }); } - return settings; }; const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.events.ops')) { + if (settings.logging?.events?.ops) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', @@ -127,11 +127,10 @@ const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', }); } - return settings; }; const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.events.request') || has(settings, 'logging.events.response')) { + if (settings.logging?.events?.request || settings.logging?.events?.response) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', @@ -142,11 +141,10 @@ const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, a 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', }); } - return settings; }; const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.timezone')) { + if (settings.logging?.timezone) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingtimezone', @@ -157,11 +155,10 @@ const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', }); } - return settings; }; const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.dest')) { + if (settings.logging?.dest) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingdest', @@ -172,11 +169,10 @@ const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprec 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', }); } - return settings; }; const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.quiet')) { + if (settings.logging?.quiet) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingquiet', @@ -185,11 +181,10 @@ const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDepre 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ', }); } - return settings; }; const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.silent')) { + if (settings.logging?.silent) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingsilent', @@ -198,11 +193,10 @@ const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDepr 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ', }); } - return settings; }; const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.verbose')) { + if (settings.logging?.verbose) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingverbose', @@ -211,7 +205,6 @@ const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDep 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ', }); } - return settings; }; const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { @@ -219,7 +212,7 @@ const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprec // the dev CLI code in src/dev/cli_dev_mode/using_server_process.ts manually // specifies `--logging.json=false`. Since it's executed in a child process, the // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY` - if (has(settings, 'logging.json') && settings.env !== 'development') { + if (settings.logging?.json && settings.env !== 'development') { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', @@ -232,11 +225,10 @@ const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprec 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', }); } - return settings; }; const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.rotate')) { + if (settings.logging?.rotate) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', @@ -247,11 +239,10 @@ const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecat 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', }); } - return settings; }; const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.events.log')) { + if (settings.logging?.events?.log) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', @@ -260,11 +251,10 @@ const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDepre 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ', }); } - return settings; }; const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.events.error')) { + if (settings.logging?.events?.error) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', @@ -273,18 +263,16 @@ const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDep 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ', }); } - return settings; }; const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (has(settings, 'logging.filter')) { + if (settings.logging?.filter) { addDeprecation({ documentationUrl: 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingfilter', message: '"logging.filter" has been deprecated and will be removed in 8.0.', }); } - return settings; }; export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts index 2eaf462768724b6..8e20e87e6f7d8e8 100644 --- a/src/core/server/config/test_utils.ts +++ b/src/core/server/config/test_utils.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { set } from '@elastic/safer-lodash-set'; import type { ConfigDeprecationProvider } from '@kbn/config'; import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; @@ -38,7 +39,7 @@ export const getDeprecationsFor = ({ settings?: Record; path: string; }) => { - return collectDeprecations(provider, { [path]: settings }, path); + return collectDeprecations(provider, set({}, path, settings), path); }; export const getDeprecationsForGlobalSettings = ({ diff --git a/src/core/server/core_app/bundle_routes/select_compressed_file.ts b/src/core/server/core_app/bundle_routes/select_compressed_file.ts index c7b071a9c354846..36c63f653618c69 100644 --- a/src/core/server/core_app/bundle_routes/select_compressed_file.ts +++ b/src/core/server/core_app/bundle_routes/select_compressed_file.ts @@ -7,10 +7,10 @@ */ import { extname } from 'path'; -import Accept from 'accept'; +import Accept from '@hapi/accept'; import { open } from './fs'; -declare module 'accept' { +declare module '@hapi/accept' { // @types/accept does not include the `preferences` argument so we override the type to include it export function encodings(encodingHeader?: string, preferences?: string[]): string[]; } diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index e731af4817955ba..b5ea0ec8c34563e 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -147,7 +147,7 @@ const deprecations: ConfigDeprecationProvider = () => [ (settings, fromPath, addDeprecation) => { const es = settings[fromPath]; if (!es) { - return settings; + return; } if (es.username === 'elastic') { addDeprecation({ @@ -171,7 +171,7 @@ const deprecations: ConfigDeprecationProvider = () => [ message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`, }); } - return settings; + return; }, ]; diff --git a/src/core/server/elasticsearch/legacy/api_types.ts b/src/core/server/elasticsearch/legacy/api_types.ts index a3b442549942f3b..e4ff4816527b481 100644 --- a/src/core/server/elasticsearch/legacy/api_types.ts +++ b/src/core/server/elasticsearch/legacy/api_types.ts @@ -140,6 +140,7 @@ import { * * @public * @deprecated + * @removeBy 7.16 */ export interface LegacyCallAPIOptions { /** @@ -157,6 +158,7 @@ export interface LegacyCallAPIOptions { /** * @deprecated + * @removeBy 7.16 * @public * */ export interface LegacyAPICaller { @@ -312,6 +314,7 @@ export interface LegacyAPICaller { /** * @deprecated + * @removeBy 7.16 * @public * */ export interface AssistantAPIClientParams extends GenericParams { @@ -321,17 +324,20 @@ export interface AssistantAPIClientParams extends GenericParams { /** * @deprecated + * @removeBy 7.16 * @public * */ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; /** * @deprecated + * @removeBy 7.16 * @public * */ export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; /** * @deprecated + * @removeBy 7.16 * @public * */ export interface AssistanceAPIResponse { @@ -344,6 +350,7 @@ export interface AssistanceAPIResponse { /** * @deprecated + * @removeBy 7.16 * @public * */ export interface DeprecationAPIClientParams extends GenericParams { @@ -353,6 +360,7 @@ export interface DeprecationAPIClientParams extends GenericParams { /** * @deprecated + * @removeBy 7.16 * @public * */ export interface DeprecationInfo { @@ -364,6 +372,7 @@ export interface DeprecationInfo { /** * @deprecated + * @removeBy 7.16 * @public * */ export interface IndexSettingsDeprecationInfo { @@ -372,6 +381,7 @@ export interface IndexSettingsDeprecationInfo { /** * @deprecated + * @removeBy 7.16 * @public * */ export interface DeprecationAPIResponse { diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index e4b72abe0bc1c16..bdb2ca4d01b3c93 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -78,6 +78,7 @@ const callAPI = async ( * See {@link LegacyClusterClient}. * * @deprecated Use {@link IClusterClient}. + * @removeBy 7.16 * @public */ export type ILegacyClusterClient = Pick; @@ -89,6 +90,7 @@ export type ILegacyClusterClient = Pick { }); }); - it('changes to available with a differemnt message when isCompatible and warningNodes present', async () => { + it('changes to available with a different message when isCompatible and warningNodes present', async () => { expect( await calculateStatus$( of({ @@ -204,4 +204,117 @@ describe('calculateStatus', () => { ] `); }); + + it('emits status updates when node info request error changes', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. connect ECONNREFUSED', + nodesInfoRequestError: new Error('connect ECONNREFUSED'), + }); + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: connect ECONNREFUSED], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. connect ECONNREFUSED", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + ] + `); + }); + + it('changes to available when a request error is resolved', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [], + incompatibleNodes: [], + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + Object { + "level": available, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Elasticsearch is available", + }, + ] + `); + }); }); diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts index 68a61b07f498e43..23e44b71863f18c 100644 --- a/src/core/server/elasticsearch/status.ts +++ b/src/core/server/elasticsearch/status.ts @@ -32,6 +32,7 @@ export const calculateStatus$ = ( message, incompatibleNodes, warningNodes, + nodesInfoRequestError, }): ServiceStatus => { if (!isCompatible) { return { @@ -40,7 +41,11 @@ export const calculateStatus$ = ( // Message should always be present, but this is a safe fallback message ?? `Some Elasticsearch nodes are not compatible with this version of Kibana`, - meta: { warningNodes, incompatibleNodes }, + meta: { + warningNodes, + incompatibleNodes, + ...(nodesInfoRequestError && { nodesInfoRequestError }), + }, }; } else if (warningNodes.length > 0) { return { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 20a191b3fa69370..8bbf665cbc0965c 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -36,6 +36,7 @@ export interface ElasticsearchServiceSetup { readonly config$: Observable; /** * @deprecated + * @removeBy 7.16 * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. * * Create application specific Elasticsearch cluster API client with customized config. See {@link ILegacyClusterClient}. @@ -60,6 +61,7 @@ export interface ElasticsearchServiceSetup { ) => ILegacyCustomClusterClient; /** + * @removeBy 7.16 * @deprecated * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. * @@ -131,6 +133,9 @@ export interface ElasticsearchServiceStart { /** * Create application specific Elasticsearch cluster API client with customized config. See {@link ILegacyClusterClient}. * + * @deprecated + * @removeBy 7.16 + * * @param type Unique identifier of the client * @param clientConfig A config consists of Elasticsearch JS client options and * valid sub-set of Elasticsearch service config. @@ -153,6 +158,9 @@ export interface ElasticsearchServiceStart { /** * A pre-configured {@link ILegacyClusterClient | legacy Elasticsearch client}. * + * @deprecated + * @removeBy 7.16 + * * @example * ```js * const client = core.elasticsearch.legacy.client; @@ -171,6 +179,7 @@ export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; export interface ElasticsearchStatusMeta { warningNodes: NodesVersionCompatibility['warningNodes']; incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; } /** diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts index 0e08fd2ddc4c5d1..70166704679fe33 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -19,7 +19,8 @@ const mockLogger = mockLoggerFactory.get('mock logger'); const KIBANA_VERSION = '5.1.0'; const createEsSuccess = elasticsearchClientMock.createSuccessTransportRequestPromise; -const createEsError = elasticsearchClientMock.createErrorTransportRequestPromise; +const createEsErrorReturn = (err: any) => + elasticsearchClientMock.createErrorTransportRequestPromise(err); function createNodes(...versions: string[]): NodesInfo { const nodes = {} as any; @@ -102,6 +103,28 @@ describe('mapNodesVersionCompatibility', () => { `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` ); }); + + it('returns isCompatible=false without an extended message when a nodesInfoRequestError is not provided', async () => { + const result = mapNodesVersionCompatibility({ nodes: {} }, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeUndefined(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes."` + ); + }); + + it('returns isCompatible=false with an extended message when a nodesInfoRequestError is present', async () => { + const result = mapNodesVersionCompatibility( + { nodes: {}, nodesInfoRequestError: new Error('connection refused') }, + KIBANA_VERSION, + false + ); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeTruthy(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes. connection refused"` + ); + }); }); describe('pollEsNodesVersion', () => { @@ -119,10 +142,10 @@ describe('pollEsNodesVersion', () => { internalClient.nodes.info.mockImplementationOnce(() => createEsSuccess(infos)); }; const nodeInfosErrorOnce = (error: any) => { - internalClient.nodes.info.mockImplementationOnce(() => createEsError(error)); + internalClient.nodes.info.mockImplementationOnce(() => createEsErrorReturn(new Error(error))); }; - it('returns iscCompatible=false and keeps polling when a poll request throws', (done) => { + it('returns isCompatible=false and keeps polling when a poll request throws', (done) => { expect.assertions(3); const expectedCompatibilityResults = [false, false, true]; jest.clearAllMocks(); @@ -148,6 +171,100 @@ describe('pollEsNodesVersion', () => { }); }); + it('returns the error from a failed nodes.info call when a poll request throws', (done) => { + expect.assertions(2); + const expectedCompatibilityResults = [false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the error from a failed nodes.info call changed from the previous poll', (done) => { + expect.assertions(4); + const expectedCompatibilityResults = [false, false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error 2', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore, same error message + nodeInfosErrorOnce('mock request error 2'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(2)) + .subscribe({ + next: (result) => { + expect(result.message).toBe(expectedMessageResults.shift()); + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns isCompatible=false and keeps polling when a poll request throws, only responding again if the error message has changed', (done) => { + expect.assertions(8); + const expectedCompatibilityResults = [false, false, true, false]; + const expectedMessageResults = [ + 'This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + "You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.2.0 @ http_address (ip), v5.1.1-Beta1 @ http_address (ip)", + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); // emit + nodeInfosErrorOnce('mock request error'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + it('returns compatibility results', (done) => { expect.assertions(1); const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index fb7ef0583e4a411..43cd52f1b572114 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -49,6 +49,7 @@ export interface NodesVersionCompatibility { incompatibleNodes: NodeInfo[]; warningNodes: NodeInfo[]; kibanaVersion: string; + nodesInfoRequestError?: Error; } function getHumanizedNodeName(node: NodeInfo) { @@ -57,22 +58,28 @@ function getHumanizedNodeName(node: NodeInfo) { } export function mapNodesVersionCompatibility( - nodesInfo: NodesInfo, + nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }, kibanaVersion: string, ignoreVersionMismatch: boolean ): NodesVersionCompatibility { - if (Object.keys(nodesInfo.nodes ?? {}).length === 0) { + if (Object.keys(nodesInfoResponse.nodes ?? {}).length === 0) { + // Note: If the a nodesInfoRequestError is present, the message contains the nodesInfoRequestError.message as a suffix + let message = `Unable to retrieve version information from Elasticsearch nodes.`; + if (nodesInfoResponse.nodesInfoRequestError) { + message = message + ` ${nodesInfoResponse.nodesInfoRequestError.message}`; + } return { isCompatible: false, - message: 'Unable to retrieve version information from Elasticsearch nodes.', + message, incompatibleNodes: [], warningNodes: [], kibanaVersion, + nodesInfoRequestError: nodesInfoResponse.nodesInfoRequestError, }; } - const nodes = Object.keys(nodesInfo.nodes) + const nodes = Object.keys(nodesInfoResponse.nodes) .sort() // Sorting ensures a stable node ordering for comparison - .map((key) => nodesInfo.nodes[key]) + .map((key) => nodesInfoResponse.nodes[key]) .map((node) => Object.assign({}, node, { name: getHumanizedNodeName(node) })); // Aggregate incompatible ES nodes. @@ -112,7 +119,13 @@ export function mapNodesVersionCompatibility( kibanaVersion, }; } - +// Returns true if NodesVersionCompatibility nodesInfoRequestError is the same +function compareNodesInfoErrorMessages( + prev: NodesVersionCompatibility, + curr: NodesVersionCompatibility +): boolean { + return prev.nodesInfoRequestError?.message === curr.nodesInfoRequestError?.message; +} // Returns true if two NodesVersionCompatibility entries match function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; @@ -121,7 +134,8 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati curr.incompatibleNodes.length === prev.incompatibleNodes.length && curr.warningNodes.length === prev.warningNodes.length && curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && - curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) && + compareNodesInfoErrorMessages(curr, prev) ); } @@ -141,14 +155,14 @@ export const pollEsNodesVersion = ({ }) ).pipe( map(({ body }) => body), - catchError((_err) => { - return of({ nodes: {} }); + catchError((nodesInfoRequestError) => { + return of({ nodes: {}, nodesInfoRequestError }); }) ); }), - map((nodesInfo: NodesInfo) => - mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + map((nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }) => + mapNodesVersionCompatibility(nodesInfoResponse, kibanaVersion, ignoreVersionMismatch) ), - distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions or if we return an error and that error changes ); }; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ca328f17b2ae15a..05408d839c0ae54 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -320,12 +320,16 @@ export type { SavedObjectsResolveResponse, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, - SavedObjectsAddToNamespacesOptions, - SavedObjectsAddToNamespacesResponse, - SavedObjectsDeleteFromNamespacesOptions, - SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, SavedObjectsServiceStart, SavedObjectsServiceSetup, SavedObjectStatusMeta, diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 468a761781365c5..6bdb8003de49ddb 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -1149,6 +1149,29 @@ describe('getSortedObjectsForExport()', () => { ]); }); + test('return results including the `namespaces` attribute when includeNamespaces option is used', async () => { + const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); + const objectResults = [ + createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), + createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), + createSavedObject({ type: 'other', id: '3' }), + ]; + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: objectResults, + }); + const exportStream = await exporter.exportByObjects({ + request, + objects: [ + { type: 'multi', id: '1' }, + { type: 'multi', id: '2' }, + { type: 'other', id: '3' }, + ], + includeNamespaces: true, + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual([...objectResults, expect.objectContaining({ exportedCount: 3 })]); + }); + test('includes nested dependencies when passed in', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 868efa872d643d7..8cd6934bf1af9cd 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -77,6 +77,7 @@ export class SavedObjectsExporter { return this.processObjects(objects, byIdAscComparator, { request: options.request, includeReferencesDeep: options.includeReferencesDeep, + includeNamespaces: options.includeNamespaces, excludeExportDetails: options.excludeExportDetails, namespace: options.namespace, }); @@ -99,6 +100,7 @@ export class SavedObjectsExporter { return this.processObjects(objects, comparator, { request: options.request, includeReferencesDeep: options.includeReferencesDeep, + includeNamespaces: options.includeNamespaces, excludeExportDetails: options.excludeExportDetails, namespace: options.namespace, }); @@ -111,6 +113,7 @@ export class SavedObjectsExporter { request, excludeExportDetails = false, includeReferencesDeep = false, + includeNamespaces = false, namespace, }: SavedObjectExportBaseOptions ) { @@ -139,9 +142,9 @@ export class SavedObjectsExporter { } // redact attributes that should not be exported - const redactedObjects = exportedObjects.map>( - ({ namespaces, ...object }) => object - ); + const redactedObjects = includeNamespaces + ? exportedObjects + : exportedObjects.map>(({ namespaces, ...object }) => object); const exportDetails: SavedObjectsExportResultDetails = { exportedCount: exportedObjects.length, diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index 4326943bd31ce2d..7891af6df5b1b51 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -15,6 +15,12 @@ export interface SavedObjectExportBaseOptions { request: KibanaRequest; /** flag to also include all related saved objects in the export stream. */ includeReferencesDeep?: boolean; + /** + * Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. + * This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP + * route for exports. + */ + includeNamespaces?: boolean; /** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */ excludeExportDetails?: boolean; /** optional namespace to override the namespace used by the savedObjectsClient. */ diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 1cf408ea96a565a..71e5565ebcbef45 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -11,6 +11,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { SavedObjectUnsanitizedDoc } from '../../serialization'; import { DocumentMigrator } from './document_migrator'; +import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { SavedObjectsType } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; @@ -724,6 +725,12 @@ describe('DocumentMigrator', () => { it('logs the original error and throws a transform error if a document transform fails', () => { const log = mockLogger; + const failedDoc = { + id: 'smelly', + type: 'dog', + attributes: {}, + migrationVersion: {}, + }; const migrator = new DocumentMigrator({ ...testOpts(), typeRegistry: createRegistry({ @@ -737,12 +744,6 @@ describe('DocumentMigrator', () => { log, }); migrator.prepareMigrations(); - const failedDoc = { - id: 'smelly', - type: 'dog', - attributes: {}, - migrationVersion: {}, - }; try { migrator.migrate(_.cloneDeep(failedDoc)); expect('Did not throw').toEqual('But it should have!'); @@ -751,6 +752,7 @@ describe('DocumentMigrator', () => { "Failed to transform document smelly. Transform: dog:1.2.3 Doc: {\\"id\\":\\"smelly\\",\\"type\\":\\"dog\\",\\"attributes\\":{},\\"migrationVersion\\":{}}" `); + expect(error).toBeInstanceOf(TransformSavedObjectDocumentError); expect(loggingSystemMock.collect(mockLoggerFactory).error[0][0]).toMatchInlineSnapshot( `[Error: Dang diggity!]` ); @@ -980,6 +982,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:loud', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'loud', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1044,6 +1047,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:cute', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'cute', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1166,6 +1170,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:hungry', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'hungry', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1238,6 +1243,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:pretty', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'pretty', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 1dd4a8fbf6388d8..c96de6ebbfcdd5d 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -62,6 +62,7 @@ import { SavedObjectsType, } from '../../types'; import { MigrationLogger } from './migration_logger'; +import { TransformSavedObjectDocumentError } from '.'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '../types'; import { DEFAULT_NAMESPACE_STRING } from '../../service/lib/utils'; @@ -559,6 +560,7 @@ function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) { id: `${namespace}:${type}:${originId}`, type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: originId, targetNamespace: namespace, targetType: type, targetId: id, @@ -659,13 +661,14 @@ function wrapWithTry( migrationFn: SavedObjectMigrationFn, log: Logger ) { + const context = Object.freeze({ + log: new MigrationLogger(log), + migrationVersion: version, + convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, + }); + return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) { try { - const context = { - log: new MigrationLogger(log), - migrationVersion: version, - convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, - }; const result = migrationFn(doc, context); // A basic sanity check to help migration authors detect basic errors @@ -679,9 +682,15 @@ function wrapWithTry( const failedTransform = `${type.name}:${version}`; const failedDoc = JSON.stringify(doc); log.error(error); - - throw new Error( - `Failed to transform document ${doc?.id}. Transform: ${failedTransform}\nDoc: ${failedDoc}` + // To make debugging failed migrations easier, we add items needed to convert the + // saved object id to the full raw id (the id only contains the uuid part) and the full error itself + throw new TransformSavedObjectDocumentError( + doc.id, + doc.type, + doc.namespace, + failedTransform, + failedDoc, + error ); } }; diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 1e51983a0ffbdba..ca54d6876ad753c 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -15,3 +15,9 @@ export type { MigrationResult, MigrationStatus } from './migration_coordinator'; export { createMigrationEsClient } from './migration_es_client'; export type { MigrationEsClient } from './migration_es_client'; export { excludeUnusedTypesQuery } from './elastic_index'; +export { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; +export type { + DocumentsTransformFailed, + DocumentsTransformSuccess, + TransformErrorObjects, +} from './migrate_raw_docs'; diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 45e73f7dfae305c..1d43e2f54a7264c 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -7,10 +7,17 @@ */ import { set } from '@elastic/safer-lodash-set'; +import * as Either from 'fp-ts/lib/Either'; import _ from 'lodash'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsSerializer } from '../../serialization'; -import { migrateRawDocs } from './migrate_raw_docs'; +import { + DocumentsTransformFailed, + DocumentsTransformSuccess, + migrateRawDocs, + migrateRawDocsSafely, +} from './migrate_raw_docs'; +import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { @@ -120,3 +127,156 @@ describe('migrateRawDocs', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`); }); }); + +describe('migrateRawDocsSafely', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('converts raw docs to saved objects', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + ]); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ] + ); + const result = (await task()) as Either.Right; + expect(result._tag).toEqual('Right'); + expect(result.right.processedDocs).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'c:d', + _source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ]); + + const obj1 = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + const obj2 = { + id: 'd', + type: 'c', + attributes: { name: 'DDD' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(2); + expect(transform).toHaveBeenNthCalledWith(1, obj1); + expect(transform).toHaveBeenNthCalledWith(2, obj2); + }); + + test('returns a `left` tag when encountering a corrupt saved object document', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'TADA'), + ]); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [ + { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ] + ); + const result = (await task()) as Either.Left; + expect(transform).toHaveBeenCalledTimes(1); + expect(result._tag).toEqual('Left'); + expect(Object.keys(result.left)).toEqual(['type', 'corruptDocumentIds', 'transformErrors']); + expect(result.left.corruptDocumentIds.length).toEqual(1); + expect(result.left.transformErrors.length).toEqual(0); + }); + + test('handles when one document is transformed into multiple documents', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + { id: 'bar', type: 'foo', attributes: { name: 'baz' } }, + ]); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] + ); + const result = (await task()) as Either.Right; + expect(result._tag).toEqual('Right'); + expect(result.right.processedDocs).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'foo:bar', + _source: { type: 'foo', foo: { name: 'baz' }, references: [] }, + }, + ]); + + const obj = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenCalledWith(obj); + }); + + test('instance of Either.left containing transform errors when the transform function throws a TransformSavedObjectDocument error', async () => { + const transform = jest.fn((doc: any) => { + throw new TransformSavedObjectDocumentError( + `${doc.id}`, + `${doc.type}`, + `${doc.namespace}`, + `${doc.type}1.2.3`, + JSON.stringify(doc), + new Error('error during transform') + ); + }); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc + ); + const result = (await task()) as Either.Left; + expect(transform).toHaveBeenCalledTimes(1); + expect(result._tag).toEqual('Left'); + expect(result.left.corruptDocumentIds.length).toEqual(0); + expect(result.left.transformErrors.length).toEqual(1); + expect(result.left.transformErrors[0].err.message).toMatchInlineSnapshot(` + "Failed to transform document b. Transform: a1.2.3 + Doc: {\\"type\\":\\"a\\",\\"id\\":\\"b\\",\\"attributes\\":{\\"name\\":\\"AAA\\"},\\"references\\":[],\\"migrationVersion\\":{}}" + `); + }); + + test("instance of Either.left containing errors when the transform function throws an error that isn't a TransformSavedObjectDocument error", async () => { + const transform = jest.fn((doc: any) => { + throw new Error('error during transform'); + }); + const task = migrateRawDocsSafely( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc + ); + const result = (await task()) as Either.Left; + expect(transform).toHaveBeenCalledTimes(1); + expect(result._tag).toEqual('Left'); + expect(result.left.corruptDocumentIds.length).toEqual(0); + expect(result.left.transformErrors.length).toEqual(1); + expect(result.left.transformErrors[0]).toMatchInlineSnapshot(` + Object { + "err": [Error: error during transform], + "rawId": "a:b", + } + `); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 102ec81646a9264..461ae1df6bc3d59 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -9,13 +9,32 @@ /* * This file provides logic for migrating raw documents. */ - +import * as TaskEither from 'fp-ts/lib/TaskEither'; +import * as Either from 'fp-ts/lib/Either'; import { + SavedObjectSanitizedDoc, SavedObjectsRawDoc, SavedObjectsSerializer, SavedObjectUnsanitizedDoc, } from '../../serialization'; import { MigrateAndConvertFn } from './document_migrator'; +import { TransformSavedObjectDocumentError } from '.'; + +export interface DocumentsTransformFailed { + readonly type: string; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; +} +export interface DocumentsTransformSuccess { + readonly processedDocs: SavedObjectsRawDoc[]; +} +export interface TransformErrorObjects { + readonly rawId: string; + readonly err: TransformSavedObjectDocumentError | Error; +} +type MigrateFn = ( + doc: SavedObjectUnsanitizedDoc +) => Promise>>; /** * Error thrown when saved object migrations encounter a corrupt saved object. @@ -37,7 +56,6 @@ export class CorruptSavedObjectError extends Error { /** * Applies the specified migration function to every saved object document in the list * of raw docs. Any raw docs that are not valid saved objects will simply be passed through. - * * @param {TransformFn} migrateDoc * @param {SavedObjectsRawDoc[]} rawDocs * @returns {SavedObjectsRawDoc[]} @@ -52,15 +70,9 @@ export async function migrateRawDocs( for (const raw of rawDocs) { const options = { namespaceTreatment: 'lax' as const }; if (serializer.isRawSavedObject(raw, options)) { - const savedObject = serializer.rawToSavedObject(raw, options); - savedObject.migrationVersion = savedObject.migrationVersion || {}; + const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); processedDocs.push( - ...(await migrateDocWithoutBlocking(savedObject)).map((attrs) => - serializer.savedObjectToRaw({ - references: [], - ...attrs, - }) - ) + ...(await migrateMapToRawDoc(migrateDocWithoutBlocking, savedObject, serializer)) ); } else { throw new CorruptSavedObjectError(raw._id); @@ -69,6 +81,58 @@ export async function migrateRawDocs( return processedDocs; } +/** + * Applies the specified migration function to every saved object document provided + * and converts the saved object to a raw document. + * Captures the ids and errors from any documents that are not valid saved objects or + * for which the transformation function failed. + * @returns {TaskEither.TaskEither} + */ +export function migrateRawDocsSafely( + serializer: SavedObjectsSerializer, + migrateDoc: MigrateAndConvertFn, + rawDocs: SavedObjectsRawDoc[] +): TaskEither.TaskEither { + return async () => { + const migrateDocNonBlocking = transformNonBlocking(migrateDoc); + const processedDocs: SavedObjectsRawDoc[] = []; + const transformErrors: TransformErrorObjects[] = []; + const corruptSavedObjectIds: string[] = []; + const options = { namespaceTreatment: 'lax' as const }; + for (const raw of rawDocs) { + if (serializer.isRawSavedObject(raw, options)) { + try { + const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); + processedDocs.push( + ...(await migrateMapToRawDoc(migrateDocNonBlocking, savedObject, serializer)) + ); + } catch (err) { + if (err instanceof TransformSavedObjectDocumentError) { + // the doc id we get from the error is only the uuid part + // we use the original raw document _id instead + transformErrors.push({ + rawId: raw._id, + err, + }); + } else { + transformErrors.push({ rawId: raw._id, err }); // cases we haven't accounted for yet + } + } + } else { + corruptSavedObjectIds.push(raw._id); + } + } + if (corruptSavedObjectIds.length > 0 || transformErrors.length > 0) { + return Either.left({ + type: 'documents_transform_failed', + corruptDocumentIds: [...corruptSavedObjectIds], + transformErrors, + }); + } + return Either.right({ processedDocs }); + }; +} + /** * Migration transform functions are potentially CPU heavy e.g. doing decryption/encryption * or (de)/serializing large JSON payloads. @@ -92,3 +156,40 @@ function transformNonBlocking( }); }); } + +/** + * Applies the specified migration function to every saved object document provided + * and converts the saved object to a raw document + * @param {MigrateFn} transformNonBlocking + * @param {SavedObjectsRawDoc[]} rawDoc + * @returns {Promise} + */ +async function migrateMapToRawDoc( + migrateMethod: MigrateFn, + savedObject: SavedObjectSanitizedDoc, + serializer: SavedObjectsSerializer +): Promise { + return [...(await migrateMethod(savedObject))].map((attrs) => + serializer.savedObjectToRaw({ + references: [], + ...attrs, + }) + ); +} + +/** + * Sanitizes the raw saved object document + * @param {SavedObjectRawDoc} rawDoc + * @param options + * @param {SavedObjectsSerializer} serializer + * @returns {SavedObjectSanitizedDoc} + */ +function convertToRawAddMigrationVersion( + rawDoc: SavedObjectsRawDoc, + options: { namespaceTreatment: 'lax' }, + serializer: SavedObjectsSerializer +): SavedObjectSanitizedDoc { + const savedObject = serializer.rawToSavedObject(rawDoc, options); + savedObject.migrationVersion = savedObject.migrationVersion || {}; + return savedObject; +} diff --git a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts new file mode 100644 index 000000000000000..80c670edd39baad --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; + +describe('TransformSavedObjectDocumentError', () => { + it('is a special error', () => { + const originalError = new Error('Dang diggity!'); + const err = new TransformSavedObjectDocumentError( + 'id', + 'type', + 'namespace', + 'failedTransform', + 'failedDoc', + originalError + ); + expect(err).toBeInstanceOf(TransformSavedObjectDocumentError); + expect(err.id).toEqual('id'); + expect(err.namespace).toEqual('namespace'); + expect(err.stack).not.toBeNull(); + }); + it('constructs an special error message', () => { + const originalError = new Error('Dang diggity!'); + const err = new TransformSavedObjectDocumentError( + 'id', + 'type', + 'namespace', + 'failedTransform', + 'failedDoc', + originalError + ); + expect(err.message).toMatchInlineSnapshot( + ` + "Failed to transform document id. Transform: failedTransform + Doc: failedDoc" + ` + ); + }); + it('handles undefined namespace', () => { + const originalError = new Error('Dang diggity!'); + const err = new TransformSavedObjectDocumentError( + 'id', + 'type', + undefined, + 'failedTransform', + 'failedDoc', + originalError + ); + expect(err.message).toMatchInlineSnapshot( + ` + "Failed to transform document id. Transform: failedTransform + Doc: failedDoc" + ` + ); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts new file mode 100644 index 000000000000000..6a6f87ea1eeb2d8 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Error thrown when saved object migrations encounter a transformation error. + * Transformation errors happen when a transform function throws an error for an unsanitized saved object + * The id (doc.id) reported in this error class is just the uuid part and doesn't tell users what the full elasticsearch id is. + * in order to convert the id to the serialized version further upstream using serializer.generateRawId, we need to provide the following items: + * - namespace: doc.namespace, + * - type: doc.type, + * - id: doc.id, + * The new error class helps with v2 migrations. + * For backward compatibility with v1 migrations, the error message is the same as what was previously thrown as a plain error + */ + +export class TransformSavedObjectDocumentError extends Error { + constructor( + public readonly id: string, + public readonly type: string, + public readonly namespace: string | undefined, + public readonly failedTransform: string, // created by document_migrator wrapWithTry as `${type.name}:${version}`; + public readonly failedDoc: string, + public readonly originalError: Error + ) { + super(`Failed to transform document ${id}. Transform: ${failedTransform}\nDoc: ${failedDoc}`); + } +} diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index e09284b49c86eef..f74fe7e7a6e1c6b 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -35,7 +35,7 @@ import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsType } from '../../types'; import { runResilientMigrator } from '../../migrationsv2'; -import { migrateRawDocs } from '../core/migrate_raw_docs'; +import { migrateRawDocsSafely } from '../core/migrate_raw_docs'; export interface KibanaMigratorOptions { client: ElasticsearchClient; @@ -135,7 +135,6 @@ export class KibanaMigrator { if (!rerun) { this.status$.next({ status: 'running' }); } - this.migrationResult = this.runMigrationsInternal().then((result) => { // Similar to above, don't publish status updates when rerunning in CI. if (!rerun) { @@ -185,7 +184,11 @@ export class KibanaMigrator { logger: this.log, preMigrationScript: indexMap[index].script, transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => - migrateRawDocs(this.serializer, this.documentMigrator.migrateAndConvert, rawDocs), + migrateRawDocsSafely( + this.serializer, + this.documentMigrator.migrateAndConvert, + rawDocs + ), migrationVersionPerType: this.documentMigrator.migrationVersion, indexPrefix: index, migrationsConfig: this.soMigrationsConfig, diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 619a7f85a327b37..570315e780ebe8d 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -56,15 +56,15 @@ export interface SavedObjectMigrationContext { /** * logger instance to be used by the migration handler */ - log: SavedObjectsMigrationLogger; + readonly log: SavedObjectsMigrationLogger; /** * The migration version that this migration function is defined for */ - migrationVersion: string; + readonly migrationVersion: string; /** * The version in which this object type is being converted to a multi-namespace type */ - convertToMultiNamespaceTypeVersion?: string; + readonly convertToMultiNamespaceTypeVersion?: string; } /** diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index ba6aafbb2f651ac..df74a4e1282e435 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -129,18 +129,6 @@ describe('actions', () => { }); }); - describe('transformDocs', () => { - it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.transformDocs(client, () => Promise.resolve([]), [], 'my_index', false); - try { - await task(); - } catch (e) { - /** ignore */ - } - expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); - }); - }); - describe('reindex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { const task = Actions.reindex( diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 79261aecf675cf9..c2e0476960c3b35 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -22,6 +22,10 @@ import { catchRetryableEsClientErrors, RetryableEsClientError, } from './catch_retryable_es_client_errors'; +import { + DocumentsTransformFailed, + DocumentsTransformSuccess, +} from '../../migrations/core/migrate_raw_docs'; export type { RetryableEsClientError }; /** @@ -46,6 +50,7 @@ export interface ActionErrorTypeMap { incompatible_mapping_exception: IncompatibleMappingException; alias_not_found_exception: AliasNotFound; remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; + documents_transform_failed: DocumentsTransformFailed; } /** @@ -451,6 +456,7 @@ export const openPit = ( export interface ReadWithPit { outdatedDocuments: SavedObjectsRawDoc[]; readonly lastHitSortValue: number[] | undefined; + readonly totalHits: number | undefined; } /* @@ -476,13 +482,20 @@ export const readWithPit = ( pit: { id: pitId, keep_alive: pitKeepAlive }, size: batchSize, search_after: searchAfter, - // Improve performance by not calculating the total number of hits - // matching the query. - track_total_hits: false, + /** + * We want to know how many documents we need to process so we can log the progress. + * But we also want to increase the performance of these requests, + * so we ask ES to report the total count only on the first request (when searchAfter does not exist) + */ + track_total_hits: typeof searchAfter === 'undefined', query, }, }) .then((response) => { + const totalHits = + typeof response.body.hits.total === 'number' + ? response.body.hits.total // This format is to be removed in 8.0 + : response.body.hits.total?.value; const hits = response.body.hits.hits; if (hits.length > 0) { @@ -490,12 +503,14 @@ export const readWithPit = ( // @ts-expect-error @elastic/elasticsearch _source is optional outdatedDocuments: hits as SavedObjectsRawDoc[], lastHitSortValue: hits[hits.length - 1].sort as number[], + totalHits, }); } return Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits, }); }) .catch(catchRetryableEsClientErrors); @@ -523,28 +538,13 @@ export const closePit = ( }; /* - * Transform outdated docs and write them to the index. + * Transform outdated docs * */ export const transformDocs = ( - client: ElasticsearchClient, transformRawDocs: TransformRawDocs, - outdatedDocuments: SavedObjectsRawDoc[], - index: string, - // used for testing purposes only - refresh: estypes.Refresh -): TaskEither.TaskEither< - RetryableEsClientError | IndexNotFound | TargetIndexHadWriteBlock, - 'bulk_index_succeeded' -> => - pipe( - TaskEither.tryCatch( - () => transformRawDocs(outdatedDocuments), - (e) => { - throw e; - } - ), - TaskEither.chain((docs) => bulkOverwriteTransformedDocuments(client, index, docs, refresh)) - ); + outdatedDocuments: SavedObjectsRawDoc[] +): TaskEither.TaskEither => + transformRawDocs(outdatedDocuments); /** @internal */ export interface ReindexResponse { @@ -747,8 +747,6 @@ export const waitForPickupUpdatedMappingsTask = flow( } ) ); - -/** @internal */ export interface AliasNotFound { type: 'alias_not_found_exception'; } diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 832d32203746532..d0158a4c68f2466 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -41,6 +41,8 @@ import { import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../migrations/core'; +import { TaskEither } from 'fp-ts/lib/TaskEither'; const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), @@ -1014,41 +1016,30 @@ describe('migration actions', () => { }); describe('transformDocs', () => { - it('applies "transformRawDocs" and writes result into an index', async () => { - const index = 'transform_docs_index'; + it('applies "transformRawDocs" and returns the transformed documents', async () => { const originalDocs = [ { _id: 'foo:1', _source: { type: 'dashboard', value: 1 } }, { _id: 'foo:2', _source: { type: 'dashboard', value: 2 } }, ]; - const createIndexTask = createIndex(client, index, { - dynamic: true, - properties: {}, - }); - await createIndexTask(); - - async function tranformRawDocs(docs: SavedObjectsRawDoc[]): Promise { - for (const doc of docs) { - doc._source.value += 1; - } - return docs; + function innerTransformRawDocs( + docs: SavedObjectsRawDoc[] + ): TaskEither { + return async () => { + const processedDocs: SavedObjectsRawDoc[] = []; + for (const doc of docs) { + doc._source.value += 1; + processedDocs.push(doc); + } + return Either.right({ processedDocs }); + }; } + const transformTask = transformDocs(innerTransformRawDocs, originalDocs); - const transformTask = transformDocs(client, tranformRawDocs, originalDocs, index, 'wait_for'); - - const result = (await transformTask()) as Either.Right<'bulk_index_succeeded'>; - - expect(result.right).toBe('bulk_index_succeeded'); - - const { body } = await client.search<{ value: number }>({ - index, - }); - const hits = body.hits.hits; - - const foo1 = hits.find((h) => h._id === 'foo:1'); - expect(foo1?._source?.value).toBe(2); - - const foo2 = hits.find((h) => h._id === 'foo:2'); + const resultsWithProcessDocs = ((await transformTask()) as Either.Right) + .right.processedDocs; + expect(resultsWithProcessDocs.length).toEqual(2); + const foo2 = resultsWithProcessDocs.find((h) => h._id === 'foo:2'); expect(foo2?._source?.value).toBe(3); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip new file mode 100644 index 000000000000000..726df7782cda3f4 Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts index 48bb282da18f637..1e494d4b558615e 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -100,7 +100,7 @@ describe('migration v2', () => { await root.setup(); await expect(root.start()).rejects.toThrow( - /Unable to migrate the corrupt saved object document with _id: 'index-pattern:test_index\*'/ + 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: index-pattern:test_index*. To allow migrations to proceed, please delete these documents.' ); const logFileContent = await asyncReadFile(logFilePath, 'utf-8'); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts new file mode 100644 index 000000000000000..e48f1e65c120f48 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; +import Util from 'util'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import { Root } from '../../../root'; + +const logFilePath = Path.join(__dirname, 'migration_test_corrupt_docs_kibana.log'); + +const asyncUnlink = Util.promisify(Fs.unlink); +async function removeLogFile() { + // ignore errors if it doesn't exist + await asyncUnlink(logFilePath).catch(() => void 0); +} + +describe('migration v2 with corrupt saved object documents', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + + beforeAll(async () => { + await removeLogFile(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('collects corrupt saved object documents accross batches', async () => { + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + // original uncorrupt SO: + // { + // type: 'foo', // 'bar', 'baz' + // foo: {}, // bar: {}, baz: {} + // migrationVersion: { + // foo: '7.13.0', + // }, + // }, + // original corrupt SO example: + // { + // id: 'bar:123' + // type: 'foo', + // foo: {}, + // migrationVersion: { + // foo: '7.13.0', + // }, + // }, + // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search + dataArchive: Path.join( + __dirname, + 'archives', + '8.0.0_migrated_with_corrupt_outdated_docs.zip' + ), + }, + }, + }); + + root = createRoot(); + + esServer = await startES(); + const coreSetup = await root.setup(); + + coreSetup.savedObjects.registerType({ + name: 'foo', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + coreSetup.savedObjects.registerType({ + name: 'bar', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + coreSetup.savedObjects.registerType({ + name: 'baz', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + try { + await root.start(); + } catch (err) { + const corruptFooSOs = /foo:/g; + const corruptBarSOs = /bar:/g; + const corruptBazSOs = /baz:/g; + expect( + [ + ...err.message.matchAll(corruptFooSOs), + ...err.message.matchAll(corruptBarSOs), + ...err.message.matchAll(corruptBazSOs), + ].length + ).toEqual(16); + } + }); +}); + +function createRoot() { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + batchSize: 5, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index 0e51c886f7f30d3..cb7f5a000cefb11 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -26,7 +26,8 @@ async function removeLogFile() { await asyncUnlink(logFilePath).catch(() => void 0); } -describe('migration from 7.7.2-xpack with 100k objects', () => { +// FAILING on 7.13: https://github.com/elastic/kibana/issues/96895 +describe.skip('migration from 7.7.2-xpack with 100k objects', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let coreStart: InternalCoreStart; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 9f7e32c49ef1532..0f4085f6186be4d 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -88,7 +88,8 @@ function createRoot() { ); } -describe('migration v2', () => { +// FAILING: https://github.com/elastic/kibana/issues/98351 +describe.skip('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; @@ -194,6 +195,7 @@ describe('migration v2', () => { id: 'legacy-url-alias:spacex:foo:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newFooId, targetNamespace: 'spacex', targetType: 'foo', @@ -226,6 +228,7 @@ describe('migration v2', () => { id: 'legacy-url-alias:spacex:bar:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newBarId, targetNamespace: 'spacex', targetType: 'bar', diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index 85cc86fe0a4682e..8443f837a7f1de4 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -10,7 +10,6 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; import * as Option from 'fp-ts/lib/Option'; import { Logger, LogMeta } from '../../logging'; import type { ElasticsearchClient } from '../../elasticsearch'; -import { CorruptSavedObjectError } from '../migrations/core/migrate_raw_docs'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { cleanup } from './migrations_state_machine_cleanup'; import { State } from './types'; @@ -74,7 +73,6 @@ const logActionResponse = ( ) => { logger.debug(logMessagePrefix + `${state.controlState} RESPONSE`, res as LogMeta); }; - const dumpExecutionLog = (logger: Logger, logMessagePrefix: string, executionLog: ExecutionLog) => { logger.error(logMessagePrefix + 'migration failed, dumping execution log:'); executionLog.forEach((log) => { @@ -211,11 +209,6 @@ export async function migrationStateActionMachine({ logger.error(e); dumpExecutionLog(logger, logMessagePrefix, executionLog); - if (e instanceof CorruptSavedObjectError) { - throw new Error( - `${e.message} To allow migrations to proceed, please delete this document from the [${initialState.indexPrefix}_${initialState.kibanaVersion}_001] index.` - ); - } const newError = new Error( `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index. ${e}` diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 213e8b43c0ea067..7a47e58f1947c79 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -36,12 +36,16 @@ import type { CloneTempToSource, SetTempWriteBlock, WaitForYellowSourceState, + TransformedDocumentsBulkIndex, + ReindexSourceToTempIndexBulk, } from './types'; import { SavedObjectsRawDoc } from '..'; import { AliasAction, RetryableEsClientError } from './actions'; import { createInitialState, model } from './model'; import { ResponseType } from './next'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import { TransformErrorObjects, TransformSavedObjectDocumentError } from '../migrations/core'; +import { createInitialProgress } from './progress'; describe('migrations v2 model', () => { const baseState: BaseState = { @@ -194,6 +198,31 @@ describe('migrations v2 model', () => { }); describe('model transitions from', () => { + it('transition returns new state', () => { + const initState: State = { + ...baseState, + controlState: 'INIT', + currentAlias: '.kibana', + versionAlias: '.kibana_7.11.0', + versionIndex: '.kibana_7.11.0_001', + }; + + const res: ResponseType<'INIT'> = Either.right({ + '.kibana_7.11.0_001': { + aliases: { + '.kibana': {}, + '.kibana_7.11.0': {}, + }, + mappings: { + properties: {}, + }, + settings: {}, + }, + }); + const newState = model(initState, res); + expect(newState).not.toBe(initState); + }); + describe('INIT', () => { const initState: State = { ...baseState, @@ -765,6 +794,8 @@ describe('migrations v2 model', () => { expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.sourceIndexPitId).toBe('pit_id'); expect(newState.lastHitSortValue).toBe(undefined); + expect(newState.progress.processed).toBe(undefined); + expect(newState.progress.total).toBe(undefined); }); }); @@ -778,6 +809,9 @@ describe('migrations v2 model', () => { targetIndex: '.kibana_7.11.0_001', tempIndexMappings: { properties: {} }, lastHitSortValue: undefined, + corruptDocumentIds: [], + transformErrors: [], + progress: createInitialProgress(), }; it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_INDEX if the index has outdated documents to reindex', () => { @@ -786,21 +820,53 @@ describe('migrations v2 model', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ outdatedDocuments, lastHitSortValue, + totalHits: 1, }); const newState = model(state, res) as ReindexSourceToTempIndex; expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_INDEX'); expect(newState.outdatedDocuments).toBe(outdatedDocuments); expect(newState.lastHitSortValue).toBe(lastHitSortValue); + expect(newState.progress.processed).toBe(undefined); + expect(newState.progress.total).toBe(1); + expect(newState.logs).toMatchInlineSnapshot(` + Array [ + Object { + "level": "info", + "message": "Starting to process 1 documents.", + }, + ] + `); }); it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_CLOSE_PIT if no outdated documents to reindex', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits: undefined, }); const newState = model(state, res) as ReindexSourceToTempClosePit; expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'); expect(newState.sourceIndexPitId).toBe('pit_id'); + expect(newState.logs).toStrictEqual([]); // No logs because no hits + }); + + it('REINDEX_SOURCE_TO_TEMP_READ -> FATAL if no outdated documents to reindex and transform failures seen with previous outdated documents', () => { + const testState: ReindexSourceToTempRead = { + ...state, + corruptDocumentIds: ['a:b'], + transformErrors: [], + }; + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ + outdatedDocuments: [], + lastHitSortValue: undefined, + totalHits: undefined, + }); + const newState = model(testState, res) as FatalState; + expect(newState.controlState).toBe('FATAL'); + expect(newState.reason).toMatchInlineSnapshot( + `"Migrations failed. Reason: Corrupt saved object documents: a:b. To allow migrations to proceed, please delete these documents."` + ); + expect(newState.logs).toStrictEqual([]); // No logs because no hits }); }); @@ -833,38 +899,112 @@ describe('migrations v2 model', () => { sourceIndexPitId: 'pit_id', targetIndex: '.kibana_7.11.0_001', lastHitSortValue: undefined, + corruptDocumentIds: [], + transformErrors: [], + progress: { processed: undefined, total: 1 }, }; + const processedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right( - 'bulk_index_succeeded' - ); - const newState = model(state, res); + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + processedDocs, + }); + const newState = model(state, res) as ReindexSourceToTempIndexBulk; + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.progress.processed).toBe(0); // Result of `(undefined ?? 0) + corruptDocumentsId.length` + }); + + it('increments the progress.processed counter', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + processedDocs, + }); + + const testState = { + ...state, + outdatedDocuments: [{ _id: '1', _source: { type: 'vis' } }], + progress: { + processed: 1, + total: 1, + }, + }; + + const newState = model(testState, res) as ReindexSourceToTempIndexBulk; + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.progress.processed).toBe(2); + }); + + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + processedDocs, + }); + const testState = { + ...state, + corruptDocumentIds: ['a:b'], + transformErrors: [], + }; + const newState = model(testState, res) as ReindexSourceToTempIndex; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + expect(newState.corruptDocumentIds.length).toEqual(1); + expect(newState.transformErrors.length).toEqual(0); + expect(newState.progress.processed).toBe(0); }); - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left target_index_had_write_block', () => { + it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ - type: 'target_index_had_write_block', + type: 'documents_transform_failed', + corruptDocumentIds: ['a:b'], + transformErrors: [], }); const newState = model(state, res) as ReindexSourceToTempRead; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); + expect(newState.corruptDocumentIds.length).toEqual(1); + expect(newState.transformErrors.length).toEqual(0); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left index_not_found_exception for temp index', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ - type: 'index_not_found_exception', - index: state.tempIndex, - }); - const newState = model(state, res) as ReindexSourceToTempRead; + }); + describe('REINDEX_SOURCE_TO_TEMP_INDEX_BULK', () => { + const transformedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; + const reindexSourceToTempIndexBulkState: ReindexSourceToTempIndexBulk = { + ...baseState, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', + transformedDocs, + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + sourceIndexPitId: 'pit_id', + targetIndex: '.kibana_7.11.0_001', + lastHitSortValue: undefined, + progress: createInitialProgress(), + }; + test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.right( + 'bulk_index_succeeded' + ); + const newState = model(reindexSourceToTempIndexBulkState, res); expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK should throw a throwBadResponse error if action failed', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({ + type: 'retryable_es_client_error', + message: 'random documents bulk index error', + }); + const newState = model(reindexSourceToTempIndexBulkState, res); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); }); describe('SET_TEMP_WRITE_BLOCK', () => { @@ -943,6 +1083,9 @@ describe('migrations v2 model', () => { targetIndex: '.kibana_7.11.0_001', lastHitSortValue: undefined, hasTransformedDocs: false, + corruptDocumentIds: [], + transformErrors: [], + progress: createInitialProgress(), }; it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_TRANSFORM if found documents to transform', () => { @@ -951,21 +1094,98 @@ describe('migrations v2 model', () => { const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ outdatedDocuments, lastHitSortValue, + totalHits: 10, }); const newState = model(state, res) as OutdatedDocumentsTransform; expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_TRANSFORM'); expect(newState.outdatedDocuments).toBe(outdatedDocuments); expect(newState.lastHitSortValue).toBe(lastHitSortValue); + expect(newState.progress.processed).toBe(undefined); + expect(newState.progress.total).toBe(10); + expect(newState.logs).toMatchInlineSnapshot(` + Array [ + Object { + "level": "info", + "message": "Starting to process 10 documents.", + }, + ] + `); + }); + + it('keeps the previous progress.total if not obtained in the result', () => { + const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; + const lastHitSortValue = [123456]; + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ + outdatedDocuments, + lastHitSortValue, + totalHits: undefined, + }); + const testState = { + ...state, + progress: { + processed: 5, + total: 10, + }, + }; + const newState = model(testState, res) as OutdatedDocumentsTransform; + expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_TRANSFORM'); + expect(newState.outdatedDocuments).toBe(outdatedDocuments); + expect(newState.lastHitSortValue).toBe(lastHitSortValue); + expect(newState.progress.processed).toBe(5); + expect(newState.progress.total).toBe(10); + expect(newState.logs).toMatchInlineSnapshot(` + Array [ + Object { + "level": "info", + "message": "Processed 5 documents out of 10.", + }, + ] + `); }); it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT if no outdated documents to transform', () => { const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ outdatedDocuments: [], lastHitSortValue: undefined, + totalHits: undefined, }); const newState = model(state, res) as OutdatedDocumentsSearchClosePit; expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'); expect(newState.pitId).toBe('pit_id'); + expect(newState.logs).toStrictEqual([]); // No logs because no hits + }); + + it('OUTDATED_DOCUMENTS_SEARCH_READ -> FATAL if no outdated documents to transform and we have failed document migrations', () => { + const corruptDocumentIdsCarriedOver = ['a:somethingelse']; + const originalTransformError = new Error('something went wrong'); + const transFormErr = new TransformSavedObjectDocumentError( + '123', + 'vis', + undefined, + 'randomvis: 7.12.0', + 'failedDoc', + originalTransformError + ); + const transformationErrors = [ + { rawId: 'bob:tail', err: transFormErr }, + ] as TransformErrorObjects[]; + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ + outdatedDocuments: [], + lastHitSortValue: undefined, + totalHits: undefined, + }); + const transformErrorsState: OutdatedDocumentsSearchRead = { + ...state, + corruptDocumentIds: [...corruptDocumentIdsCarriedOver], + transformErrors: [...transformationErrors], + }; + const newState = model(transformErrorsState, res) as FatalState; + expect(newState.controlState).toBe('FATAL'); + expect(newState.reason.includes('Migrations failed. Reason:')).toBe(true); + expect(newState.reason.includes('Corrupt saved object documents: ')).toBe(true); + expect(newState.reason.includes('Transformation errors: ')).toBe(true); + expect(newState.reason.includes('randomvis: 7.12.0')).toBe(true); + expect(newState.logs).toStrictEqual([]); // No logs because no hits }); }); @@ -1006,9 +1226,20 @@ describe('migrations v2 model', () => { }); describe('OUTDATED_DOCUMENTS_TRANSFORM', () => { - const outdatedDocuments = ([ - Symbol('raw saved object doc'), - ] as unknown) as SavedObjectsRawDoc[]; + const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; + const corruptDocumentIds = ['a:somethingelse']; + const originalTransformError = new Error('Dang diggity!'); + const transFormErr = new TransformSavedObjectDocumentError( + 'id', + 'type', + 'namespace', + 'failedTransform', + 'failedDoc', + originalTransformError + ); + const transformationErrors = [ + { rawId: 'bob:tail', err: transFormErr }, + ] as TransformErrorObjects[]; const outdatedDocumentsTransformState: OutdatedDocumentsTransform = { ...baseState, controlState: 'OUTDATED_DOCUMENTS_TRANSFORM', @@ -1016,18 +1247,139 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', outdatedDocuments, + corruptDocumentIds: [], + transformErrors: [], pitId: 'pit_id', lastHitSortValue: [3, 4], hasTransformedDocs: false, + progress: createInitialProgress(), }; - test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if action succeeds', () => { - const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right( - 'bulk_index_succeeded' - ); - const newState = model(outdatedDocumentsTransformState, res); - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + describe('OUTDATED_DOCUMENTS_TRANSFORM if action succeeds', () => { + const processedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; + test('OUTDATED_DOCUMENTS_TRANSFORM -> TRANSFORMED_DOCUMENTS_BULK_INDEX if action succeeds', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right({ processedDocs }); + const newState = model( + outdatedDocumentsTransformState, + res + ) as TransformedDocumentsBulkIndex; + expect(newState.controlState).toEqual('TRANSFORMED_DOCUMENTS_BULK_INDEX'); + expect(newState.transformedDocs).toEqual(processedDocs); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + expect(newState.progress.processed).toBe(outdatedDocuments.length); + }); + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if there are are existing documents that failed transformation', () => { + const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { + ...outdatedDocumentsTransformState, + corruptDocumentIds: [...corruptDocumentIds], + transformErrors: [], + }; + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right({ processedDocs }); + const newState = model( + outdatedDocumentsTransformStateWithFailedDocuments, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds).toEqual(corruptDocumentIds); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + expect(newState.progress.processed).toBe(outdatedDocuments.length); + }); + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if there are are existing documents that failed transformation because of transform errors', () => { + const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { + ...outdatedDocumentsTransformState, + corruptDocumentIds: [], + transformErrors: [...transformationErrors], + }; + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right({ processedDocs }); + const newState = model( + outdatedDocumentsTransformStateWithFailedDocuments, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds.length).toEqual(0); + expect(newState.transformErrors.length).toEqual(1); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + expect(newState.progress.processed).toBe(outdatedDocuments.length); + }); + }); + describe('OUTDATED_DOCUMENTS_TRANSFORM if action fails', () => { + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ adding newly failed documents to state if documents failed the transform', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.left({ + type: 'documents_transform_failed', + corruptDocumentIds, + transformErrors: [], + }); + const newState = model( + outdatedDocumentsTransformState, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds).toEqual(corruptDocumentIds); + expect(newState.progress.processed).toBe(outdatedDocuments.length); + }); + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ combines newly failed documents with those already on state if documents failed the transform', () => { + const newFailedTransformDocumentIds = ['b:other', 'c:__']; + const outdatedDocumentsTransformStateWithFailedDocuments: OutdatedDocumentsTransform = { + ...outdatedDocumentsTransformState, + corruptDocumentIds: [...corruptDocumentIds], + transformErrors: [...transformationErrors], + }; + const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.left({ + type: 'documents_transform_failed', + corruptDocumentIds: newFailedTransformDocumentIds, + transformErrors: transformationErrors, + }); + const newState = model( + outdatedDocumentsTransformStateWithFailedDocuments, + res + ) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.corruptDocumentIds).toEqual([ + ...corruptDocumentIds, + ...newFailedTransformDocumentIds, + ]); + expect(newState.progress.processed).toBe(outdatedDocuments.length); + }); + }); + }); + describe('TRANSFORMED_DOCUMENTS_BULK_INDEX', () => { + const transformedDocs = [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ] as SavedObjectsRawDoc[]; + const transformedDocumentsBulkIndexState: TransformedDocumentsBulkIndex = { + ...baseState, + controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', + transformedDocs, + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + targetIndex: '.kibana_7.11.0_001', + pitId: 'pit_id', + lastHitSortValue: [3, 4], + hasTransformedDocs: false, + progress: createInitialProgress(), + }; + test('TRANSFORMED_DOCUMENTS_BULK_INDEX should throw a throwBadResponse error if action failed', () => { + const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.left({ + type: 'retryable_es_client_error', + message: 'random documents bulk index error', + }); + const newState = model( + transformedDocumentsBulkIndexState, + res + ) as TransformedDocumentsBulkIndex; + expect(newState.controlState).toEqual('TRANSFORMED_DOCUMENTS_BULK_INDEX'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 318eff19d5e24d7..f4185225ae0732d 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -9,15 +9,21 @@ import { gt, valid } from 'semver'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; -import { cloneDeep } from 'lodash'; + import { AliasAction, FetchIndexResponse, isLeftTypeof, RetryableEsClientError } from './actions'; import { AllActionStates, InitState, State } from './types'; import { IndexMapping } from '../mappings'; import { ResponseType } from './next'; import { SavedObjectsMigrationVersion } from '../types'; import { disableUnknownTypeMappingFields } from '../migrations/core/migration_context'; -import { excludeUnusedTypesQuery } from '../migrations/core'; +import { excludeUnusedTypesQuery, TransformErrorObjects } from '../migrations/core'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import { + createInitialProgress, + incrementProcessedProgress, + logProgress, + setProgressTotal, +} from './progress'; /** * A helper function/type for ensuring that all control state's are handled. @@ -97,6 +103,31 @@ function getAliases(indices: FetchIndexResponse) { }, {} as Record); } +/** + * Constructs migration failure message strings from corrupt document ids and document transformation errors + */ +function extractTransformFailuresReason( + corruptDocumentIds: string[], + transformErrors: TransformErrorObjects[] +): { corruptDocsReason: string; transformErrsReason: string } { + const corruptDocumentIdReason = + corruptDocumentIds.length > 0 + ? ` Corrupt saved object documents: ${corruptDocumentIds.join(',')}` + : ''; + // we have both the saved object Id and the stack trace in each `transformErrors` item. + const transformErrorsReason = + transformErrors.length > 0 + ? ' Transformation errors: ' + + transformErrors + .map((errObj) => `${errObj.rawId}: ${errObj.err.message}\n ${errObj.err.stack ?? ''}`) + .join('/n') + : ''; + return { + corruptDocsReason: corruptDocumentIdReason, + transformErrsReason: transformErrorsReason, + }; +} + const delayRetryState = ( state: S, errorMessage: string, @@ -156,7 +187,7 @@ export const model = (currentState: State, resW: ResponseType): // control state using: // `const res = resW as ResponseType;` - let stateP: State = cloneDeep(currentState); + let stateP: State = currentState; // Handle retryable_es_client_errors. Other left values need to be handled // by the control state specific code below. @@ -481,25 +512,51 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'REINDEX_SOURCE_TO_TEMP_READ', sourceIndexPitId: res.right.pitId, lastHitSortValue: undefined, + // placeholders to collect document transform problems + corruptDocumentIds: [], + transformErrors: [], + progress: createInitialProgress(), }; } else { throwBadResponse(stateP, res); } } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_READ') { + // we carry through any failures we've seen with transforming documents on state const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { + const progress = setProgressTotal(stateP.progress, res.right.totalHits); + const logs = logProgress(stateP.logs, progress); if (res.right.outdatedDocuments.length > 0) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, + progress, + logs, }; + } else { + // we don't have any more outdated documents and need to either fail or move on to updating the target mappings. + if (stateP.corruptDocumentIds.length > 0 || stateP.transformErrors.length > 0) { + const { corruptDocsReason, transformErrsReason } = extractTransformFailuresReason( + stateP.corruptDocumentIds, + stateP.transformErrors + ); + return { + ...stateP, + controlState: 'FATAL', + reason: `Migrations failed. Reason:${corruptDocsReason}${transformErrsReason}. To allow migrations to proceed, please delete these documents.`, + }; + } else { + // we don't have any more outdated documents and we haven't encountered any document transformation issues. + // Close the PIT search and carry on with the happy path. + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', + logs, + }; + } } - return { - ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', - }; } else { throwBadResponse(stateP, res); } @@ -516,34 +573,63 @@ export const model = (currentState: State, resW: ResponseType): throwBadResponse(stateP, res); } } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX') { + // We follow a similar control flow as for + // outdated document search -> outdated document transform -> transform documents bulk index + // collecting issues along the way rather than failing + // REINDEX_SOURCE_TO_TEMP_INDEX handles the document transforms const res = resW as ExcludeRetryableEsError>; + + // Increment the processed documents, no matter what the results are. + // Otherwise the progress might look off when there are errors. + const progress = incrementProcessedProgress(stateP.progress, stateP.outdatedDocuments.length); + if (Either.isRight(res)) { - return { - ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_READ', - }; + if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', // handles the actual bulk indexing into temp index + transformedDocs: [...res.right.processedDocs], + progress, + }; + } else { + // we don't have any transform issues with the current batch of outdated docs but + // we have carried through previous transformation issues. + // The migration will ultimately fail but before we do that, continue to + // search through remaining docs for more issues and pass the previous failures along on state + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + progress, + }; + } } else { + // we have failures from the current batch of documents and add them to the lists const left = res.left; - if ( - isLeftTypeof(left, 'target_index_had_write_block') || - (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.tempIndex) - ) { - // index_not_found_exception: - // another instance completed the MARK_VERSION_INDEX_READY and - // removed the temp index. - // target_index_had_write_block - // another instance completed the SET_TEMP_WRITE_BLOCK step adding a - // write block to the temp index. - // - // For simplicity we continue linearly through the next steps even if - // we know another instance already completed these. + if (isLeftTypeof(left, 'documents_transform_failed')) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + corruptDocumentIds: [...stateP.corruptDocumentIds, ...left.corruptDocumentIds], + transformErrors: [...stateP.transformErrors, ...left.transformErrors], + progress, }; + } else { + // should never happen + throwBadResponse(stateP, res as never); } - // should never happen - throwBadResponse(stateP, res as never); + } + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + // we're still on the happy path with no transformation failures seen. + corruptDocumentIds: [], + transformErrors: [], + }; + } else { + throwBadResponse(stateP, res); } } else if (stateP.controlState === 'SET_TEMP_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; @@ -610,7 +696,10 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', pitId: res.right.pitId, lastHitSortValue: undefined, + progress: createInitialProgress(), hasTransformedDocs: false, + corruptDocumentIds: [], + transformErrors: [], }; } else { throwBadResponse(stateP, res); @@ -619,66 +708,131 @@ export const model = (currentState: State, resW: ResponseType): const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { if (res.right.outdatedDocuments.length > 0) { + const progress = setProgressTotal(stateP.progress, res.right.totalHits); + const logs = logProgress(stateP.logs, progress); + return { ...stateP, controlState: 'OUTDATED_DOCUMENTS_TRANSFORM', outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, + progress, + logs, + }; + } else { + // we don't have any more outdated documents and need to either fail or move on to updating the target mappings. + if (stateP.corruptDocumentIds.length > 0 || stateP.transformErrors.length > 0) { + const { corruptDocsReason, transformErrsReason } = extractTransformFailuresReason( + stateP.corruptDocumentIds, + stateP.transformErrors + ); + return { + ...stateP, + controlState: 'FATAL', + reason: `Migrations failed. Reason:${corruptDocsReason}${transformErrsReason}. To allow migrations to proceed, please delete these documents.`, + }; + } else { + // If there are no more results we have transformed all outdated + // documents and we didn't encounter any corrupt documents or transformation errors + // and can proceed to the next step + return { + ...stateP, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', + }; + } + } + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_TRANSFORM') { + const res = resW as ExcludeRetryableEsError>; + + // Increment the processed documents, no matter what the results are. + // Otherwise the progress might look off when there are errors. + const progress = incrementProcessedProgress(stateP.progress, stateP.outdatedDocuments.length); + + if (Either.isRight(res)) { + // we haven't seen corrupt documents or any transformation errors thus far in the migration + // index the migrated docs + if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { + return { + ...stateP, + controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', + transformedDocs: [...res.right.processedDocs], + hasTransformedDocs: true, + progress, }; } else { + // We have seen corrupt documents and/or transformation errors + // skip indexing and go straight to reading and transforming more docs return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + progress, }; } } else { - throwBadResponse(stateP, res); + if (isLeftTypeof(res.left, 'documents_transform_failed')) { + // continue to build up any more transformation errors before failing the migration. + return { + ...stateP, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + corruptDocumentIds: [...stateP.corruptDocumentIds, ...res.left.corruptDocumentIds], + transformErrors: [...stateP.transformErrors, ...res.left.transformErrors], + hasTransformedDocs: false, + progress, + }; + } else { + throwBadResponse(stateP, res as never); + } } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_REFRESH') { + } else if (stateP.controlState === 'TRANSFORMED_DOCUMENTS_BULK_INDEX') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, - controlState: 'UPDATE_TARGET_MAPPINGS', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + corruptDocumentIds: [], + transformErrors: [], + hasTransformedDocs: true, }; } else { throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT') { + } else if (stateP.controlState === 'UPDATE_TARGET_MAPPINGS') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - const { pitId, hasTransformedDocs, ...state } = stateP; - if (hasTransformedDocs) { - return { - ...state, - controlState: 'OUTDATED_DOCUMENTS_REFRESH', - }; - } return { - ...state, - controlState: 'UPDATE_TARGET_MAPPINGS', + ...stateP, + controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK', + updateTargetMappingsTaskId: res.right.taskId, }; } else { - throwBadResponse(stateP, res); + throwBadResponse(stateP, res as never); } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_TRANSFORM') { + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_REFRESH') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', - hasTransformedDocs: true, + controlState: 'UPDATE_TARGET_MAPPINGS', }; } else { - throwBadResponse(stateP, res as never); + throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'UPDATE_TARGET_MAPPINGS') { + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { + const { pitId, hasTransformedDocs, ...state } = stateP; + if (hasTransformedDocs) { + return { + ...state, + controlState: 'OUTDATED_DOCUMENTS_REFRESH', + }; + } return { - ...stateP, - controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK', - updateTargetMappingsTaskId: res.right.taskId, + ...state, + controlState: 'UPDATE_TARGET_MAPPINGS', }; } else { throwBadResponse(stateP, res); diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 536c07d6a071d92..07ebf80271d48c5 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -32,6 +32,8 @@ import type { SetTempWriteBlock, WaitForYellowSourceState, TransformRawDocs, + TransformedDocumentsBulkIndex, + ReindexSourceToTempIndexBulk, OutdatedDocumentsSearchOpenPit, OutdatedDocumentsSearchRead, OutdatedDocumentsSearchClosePit, @@ -82,11 +84,12 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => Actions.closePit(client, state.sourceIndexPitId), REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => - Actions.transformDocs( + Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) => + Actions.bulkOverwriteTransformedDocuments( client, - transformRawDocs, - state.outdatedDocuments, state.tempIndex, + state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -121,11 +124,12 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra OUTDATED_DOCUMENTS_REFRESH: (state: OutdatedDocumentsRefresh) => Actions.refreshIndex(client, state.targetIndex), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => - Actions.transformDocs( + Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + TRANSFORMED_DOCUMENTS_BULK_INDEX: (state: TransformedDocumentsBulkIndex) => + Actions.bulkOverwriteTransformedDocuments( client, - transformRawDocs, - state.outdatedDocuments, state.targetIndex, + state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. diff --git a/src/core/server/saved_objects/migrationsv2/progress.test.ts b/src/core/server/saved_objects/migrationsv2/progress.test.ts new file mode 100644 index 000000000000000..a0d89c2c63300fa --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/progress.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { MigrationLog } from './types'; +import { + createInitialProgress, + incrementProcessedProgress, + logProgress, + setProgressTotal, +} from './progress'; + +describe('createInitialProgress', () => { + test('create initial progress', () => { + expect(createInitialProgress()).toStrictEqual({ + processed: undefined, + total: undefined, + }); + }); +}); + +describe('setProgressTotal', () => { + const previousProgress = { + processed: undefined, + total: 10, + }; + test('should keep the previous total if not provided', () => { + expect(setProgressTotal(previousProgress)).toStrictEqual(previousProgress); + }); + + test('should keep the previous total is undefined', () => { + expect(setProgressTotal(previousProgress, undefined)).toStrictEqual(previousProgress); + }); + + test('should overwrite if the previous total is provided', () => { + expect(setProgressTotal(previousProgress, 20)).toStrictEqual({ + processed: undefined, + total: 20, + }); + }); +}); + +describe('logProgress', () => { + const previousLogs: MigrationLog[] = []; + + test('should not log anything if there is no total', () => { + const progress = { + processed: undefined, + total: undefined, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([]); + }); + + test('should not log anything if total is 0', () => { + const progress = { + processed: undefined, + total: 0, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([]); + }); + + test('should log the "Starting..." log', () => { + const progress = { + processed: undefined, + total: 10, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([ + { + level: 'info', + message: 'Starting to process 10 documents.', + }, + ]); + }); + + test('should log the "Processed..." log', () => { + const progress = { + processed: 5, + total: 10, + }; + expect(logProgress(previousLogs, progress)).toStrictEqual([ + { + level: 'info', + message: 'Processed 5 documents out of 10.', + }, + ]); + }); +}); + +describe('incrementProcessedProgress', () => { + const previousProgress = { + processed: undefined, + total: 10, + }; + test('should not increment if the incrementValue is not defined', () => { + expect(incrementProcessedProgress(previousProgress)).toStrictEqual({ + processed: 0, + total: 10, + }); + }); + + test('should not increment if the incrementValue is undefined', () => { + expect(incrementProcessedProgress(previousProgress, undefined)).toStrictEqual({ + processed: 0, + total: 10, + }); + }); + + test('should not increment if the incrementValue is not defined (with some processed values)', () => { + const testPreviousProgress = { + ...previousProgress, + processed: 1, + }; + expect(incrementProcessedProgress(testPreviousProgress, undefined)).toStrictEqual({ + processed: 1, + total: 10, + }); + }); + + test('should increment if the incrementValue is defined', () => { + expect(incrementProcessedProgress(previousProgress, 5)).toStrictEqual({ + processed: 5, + total: 10, + }); + }); + + test('should increment if the incrementValue is defined (with some processed values)', () => { + const testPreviousProgress = { + ...previousProgress, + processed: 5, + }; + expect(incrementProcessedProgress(testPreviousProgress, 5)).toStrictEqual({ + processed: 10, + total: 10, + }); + }); +}); diff --git a/src/core/server/saved_objects/migrationsv2/progress.ts b/src/core/server/saved_objects/migrationsv2/progress.ts new file mode 100644 index 000000000000000..d626cd652890222 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/progress.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { MigrationLog, Progress } from './types'; + +/** + * Returns an initial state of the progress object (everything undefined) + */ +export function createInitialProgress(): Progress { + return { + processed: undefined, + total: undefined, + }; +} + +/** + * Overwrites the total of the progress if anything provided + * @param previousProgress + * @param total + */ +export function setProgressTotal( + previousProgress: Progress, + total = previousProgress.total +): Progress { + return { + ...previousProgress, + total, + }; +} + +/** + * Returns a new list of MigrationLogs with the info entry about the progress + * @param previousLogs + * @param progress + */ +export function logProgress(previousLogs: MigrationLog[], progress: Progress): MigrationLog[] { + const logs = [...previousLogs]; + + if (progress.total) { + if (typeof progress.processed === 'undefined') { + logs.push({ + level: 'info', + message: `Starting to process ${progress.total} documents.`, + }); + } else { + logs.push({ + level: 'info', + message: `Processed ${progress.processed} documents out of ${progress.total}.`, + }); + } + } + + return logs; +} + +/** + * Increments the processed count and returns a new Progress + * @param previousProgress Previous state of the progress + * @param incrementProcessedBy Amount to increase the processed count by + */ +export function incrementProcessedProgress( + previousProgress: Progress, + incrementProcessedBy = 0 +): Progress { + return { + ...previousProgress, + processed: (previousProgress.processed ?? 0) + incrementProcessedBy, + }; +} diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index ac807e9d617768e..adcd2ad32fd24e6 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -6,12 +6,18 @@ * Side Public License, v 1. */ +import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; import { estypes } from '@elastic/elasticsearch'; import { ControlState } from './state_action_machine'; import { AliasAction } from './actions'; import { IndexMapping } from '../mappings'; import { SavedObjectsRawDoc } from '..'; +import { TransformErrorObjects } from '../migrations/core'; +import { + DocumentsTransformFailed, + DocumentsTransformSuccess, +} from '../migrations/core/migrate_raw_docs'; export type MigrationLogLevel = 'error' | 'info'; @@ -20,6 +26,11 @@ export interface MigrationLog { message: string; } +export interface Progress { + processed: number | undefined; + total: number | undefined; +} + export interface BaseState extends ControlState { /** The first part of the index name such as `.kibana` or `.kibana_task_manager` */ readonly indexPrefix: string; @@ -175,6 +186,9 @@ export interface ReindexSourceToTempRead extends PostInitState { readonly controlState: 'REINDEX_SOURCE_TO_TEMP_READ'; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; } export interface ReindexSourceToTempClosePit extends PostInitState { @@ -187,6 +201,17 @@ export interface ReindexSourceToTempIndex extends PostInitState { readonly outdatedDocuments: SavedObjectsRawDoc[]; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; +} + +export interface ReindexSourceToTempIndexBulk extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'; + readonly transformedDocs: SavedObjectsRawDoc[]; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; + readonly progress: Progress; } export type SetTempWriteBlock = PostInitState & { @@ -233,6 +258,9 @@ export interface OutdatedDocumentsSearchRead extends PostInitState { readonly pitId: string; readonly lastHitSortValue: number[] | undefined; readonly hasTransformedDocs: boolean; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; } export interface OutdatedDocumentsSearchClosePit extends PostInitState { @@ -249,12 +277,26 @@ export interface OutdatedDocumentsRefresh extends PostInitState { } export interface OutdatedDocumentsTransform extends PostInitState { - /** Transform a batch of outdated documents to their latest version and write them to the target index */ + /** Transform a batch of outdated documents to their latest version*/ readonly controlState: 'OUTDATED_DOCUMENTS_TRANSFORM'; readonly pitId: string; readonly outdatedDocuments: SavedObjectsRawDoc[]; readonly lastHitSortValue: number[] | undefined; readonly hasTransformedDocs: boolean; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; +} +export interface TransformedDocumentsBulkIndex extends PostInitState { + /** + * Write the up-to-date transformed documents to the target index + */ + readonly controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX'; + readonly transformedDocs: SavedObjectsRawDoc[]; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; + readonly pitId: string; + readonly progress: Progress; } export interface MarkVersionIndexReady extends PostInitState { @@ -339,7 +381,7 @@ export interface LegacyDeleteState extends LegacyBaseState { readonly controlState: 'LEGACY_DELETE'; } -export type State = +export type State = Readonly< | FatalState | InitState | DoneState @@ -351,6 +393,7 @@ export type State = | ReindexSourceToTempRead | ReindexSourceToTempClosePit | ReindexSourceToTempIndex + | ReindexSourceToTempIndexBulk | SetTempWriteBlock | CloneTempToSource | UpdateTargetMappingsState @@ -363,11 +406,13 @@ export type State = | OutdatedDocumentsRefresh | MarkVersionIndexReady | MarkVersionIndexReadyConflict + | TransformedDocumentsBulkIndex | LegacyCreateReindexTargetState | LegacySetWriteBlockState | LegacyReindexState | LegacyReindexWaitForTaskState - | LegacyDeleteState; + | LegacyDeleteState +>; export type AllControlStates = State['controlState']; /** @@ -376,4 +421,6 @@ export type AllControlStates = State['controlState']; */ export type AllActionStates = Exclude; -export type TransformRawDocs = (rawDocs: SavedObjectsRawDoc[]) => Promise; +export type TransformRawDocs = ( + rawDocs: SavedObjectsRawDoc[] +) => TaskEither.TaskEither; diff --git a/src/core/server/saved_objects/object_types/registration.ts b/src/core/server/saved_objects/object_types/registration.ts index 149fc09ce401d78..2b5f49123b2cf82 100644 --- a/src/core/server/saved_objects/object_types/registration.ts +++ b/src/core/server/saved_objects/object_types/registration.ts @@ -13,10 +13,15 @@ const legacyUrlAliasType: SavedObjectsType = { name: LEGACY_URL_ALIAS_TYPE, namespaceType: 'agnostic', mappings: { - dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields - properties: {}, + dynamic: false, + properties: { + sourceId: { type: 'keyword' }, + targetType: { type: 'keyword' }, + disabled: { type: 'boolean' }, + // other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above) + }, }, - hidden: true, + hidden: false, }; /** diff --git a/src/core/server/saved_objects/object_types/types.ts b/src/core/server/saved_objects/object_types/types.ts index 6fca2ed59906b81..9038d1a606067be 100644 --- a/src/core/server/saved_objects/object_types/types.ts +++ b/src/core/server/saved_objects/object_types/types.ts @@ -7,13 +7,49 @@ */ /** + * A legacy URL alias is created for an object when it is converted from a single-namespace type to a multi-namespace type. This enables us + * to preserve functionality of existing URLs for objects whose IDs have been changed during the conversion process, by way of the new + * `SavedObjectsClient.resolve()` API. + * + * Legacy URL aliases are only created by the `DocumentMigrator`, and will always have a saved object ID as follows: + * + * ``` + * `${targetNamespace}:${targetType}:${sourceId}` + * ``` + * + * This predictable object ID allows aliases to be easily looked up during the resolve operation, and ensures that exactly one alias will + * exist for a given source per space. + * * @internal */ export interface LegacyUrlAlias { + /** + * The original ID of the object, before it was converted. + */ + sourceId: string; + /** + * The namespace that the object existed in when it was converted. + */ targetNamespace: string; + /** + * The type of the object when it was converted. + */ targetType: string; + /** + * The new ID of the object when it was converted. + */ targetId: string; + /** + * The last time this alias was used with `SavedObjectsClient.resolve()`. + */ lastResolved?: string; + /** + * How many times this alias was used with `SavedObjectsClient.resolve()`. + */ resolveCounter?: number; + /** + * If true, this alias is disabled and it will be ignored in `SavedObjectsClient.resolve()` and + * `SavedObjectsClient.collectMultiNamespaceReferences()`. + */ disabled?: boolean; } diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 4b955032939b38d..9c91abcfe79c56d 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -76,10 +76,10 @@ export class SavedObjectsSerializer { * @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format. * @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document. */ - public rawToSavedObject( + public rawToSavedObject( doc: SavedObjectsRawDoc, options: SavedObjectsRawDocParseOptions = {} - ): SavedObjectSanitizedDoc { + ): SavedObjectSanitizedDoc { this.checkIsRawSavedObject(doc, options); // throws a descriptive error if the document is not a saved object const { namespaceTreatment = 'strict' } = options; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 8a66e6176d1f5a9..7b4ffcf2dd6cf05 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -17,6 +17,14 @@ export type { SavedObjectsClientWrapperOptions, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts new file mode 100644 index 000000000000000..cbd1ac4a8eb8f95 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as InternalUtils from './internal_utils'; + +export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< + typeof InternalUtils['rawDocExistsInNamespace'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + rawDocExistsInNamespace: mockRawDocExistsInNamespace, + }; +}); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts new file mode 100644 index 000000000000000..00fc039ff005fd4 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts @@ -0,0 +1,444 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mockRawDocExistsInNamespace } from './collect_multi_namespace_references.test.mock'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { SavedObjectsSerializer } from '../../serialization'; +import type { + CollectMultiNamespaceReferencesParams, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, +} from './collect_multi_namespace_references'; +import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; +import { savedObjectsRepositoryMock } from './repository.mock'; +import { PointInTimeFinder } from './point_in_time_finder'; +import { ISavedObjectsRepository } from './repository'; + +const SPACES = ['default', 'another-space']; +const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; + +const MULTI_NAMESPACE_OBJ_TYPE_1 = 'type-a'; +const MULTI_NAMESPACE_OBJ_TYPE_2 = 'type-b'; +const NON_MULTI_NAMESPACE_OBJ_TYPE = 'type-c'; +const MULTI_NAMESPACE_HIDDEN_OBJ_TYPE = 'type-d'; + +beforeEach(() => { + mockRawDocExistsInNamespace.mockReset(); + mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default +}); + +describe('collectMultiNamespaceReferences', () => { + let client: DeeplyMockedKeys; + let savedObjectsMock: jest.Mocked; + let createPointInTimeFinder: jest.MockedFunction< + CollectMultiNamespaceReferencesParams['createPointInTimeFinder'] + >; + let pointInTimeFinder: DeeplyMockedKeys; + + /** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `collectMultiNamespaceReferences` */ + function setup( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): CollectMultiNamespaceReferencesParams { + const registry = typeRegistryMock.create(); + registry.isMultiNamespace.mockImplementation( + (type) => + [ + MULTI_NAMESPACE_OBJ_TYPE_1, + MULTI_NAMESPACE_OBJ_TYPE_2, + MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, + ].includes(type) // NON_MULTI_NAMESPACE_TYPE is omitted + ); + registry.isShareable.mockImplementation( + (type) => [MULTI_NAMESPACE_OBJ_TYPE_1, MULTI_NAMESPACE_HIDDEN_OBJ_TYPE].includes(type) // MULTI_NAMESPACE_OBJ_TYPE_2 and NON_MULTI_NAMESPACE_TYPE are omitted + ); + client = elasticsearchClientMock.createElasticsearchClient(); + + const serializer = new SavedObjectsSerializer(registry); + savedObjectsMock = savedObjectsRepositoryMock.create(); + savedObjectsMock.find.mockResolvedValue({ + pit_id: 'foo', + saved_objects: [], + // the rest of these fields don't matter but are included for type safety + total: 0, + page: 1, + per_page: 100, + }); + createPointInTimeFinder = jest.fn(); + createPointInTimeFinder.mockImplementation((params) => { + pointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ savedObjectsMock })(params); + return pointInTimeFinder; + }); + return { + registry, + allowedTypes: [ + MULTI_NAMESPACE_OBJ_TYPE_1, + MULTI_NAMESPACE_OBJ_TYPE_2, + NON_MULTI_NAMESPACE_OBJ_TYPE, + ], // MULTI_NAMESPACE_HIDDEN_TYPE is omitted + client, + serializer, + getIndexForType: (type: string) => `index-for-${type}`, + createPointInTimeFinder, + objects, + options, + }; + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockMgetResults( + ...results: Array<{ + found: boolean; + references?: Array<{ type: string; id: string }>; + }> + ) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + docs: results.map((x) => { + const references = + x.references?.map(({ type, id }) => ({ type, id, name: 'ref-name' })) ?? []; + return x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { + namespaces: SPACES, + references, + }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + }; + }), + }) + ); + } + + function mockFindResults(...results: LegacyUrlAlias[]) { + savedObjectsMock.find.mockResolvedValueOnce({ + pit_id: 'foo', + saved_objects: results.map((attributes) => ({ + id: 'doesnt-matter', + type: LEGACY_URL_ALIAS_TYPE, + attributes, + references: [], + score: 0, // doesn't matter + })), + // the rest of these fields don't matter but are included for type safety + total: 0, + page: 1, + per_page: 100, + }); + } + + /** Asserts that mget is called for the given objects */ + function expectMgetArgs( + n: number, + ...objects: SavedObjectsCollectMultiNamespaceReferencesObject[] + ) { + const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); + expect(client.mget).toHaveBeenNthCalledWith(n, { body: { docs } }, expect.anything()); + } + + it('returns an empty array if no object args are passed in', async () => { + const params = setup([]); + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).not.toHaveBeenCalled(); + expect(result.objects).toEqual([]); + }); + + it('excludes args that have unsupported types', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' }; + const params = setup([obj1, obj2, obj3]); + mockMgetResults({ found: true }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // the non-multi-namespace type and the hidden type are excluded + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // even though they are excluded from the cluster call, obj2 and obj3 are included in the results + { ...obj2, spaces: [], inboundReferences: [] }, + { ...obj3, spaces: [], inboundReferences: [] }, + ]); + }); + + it('excludes references that have unsupported types', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj2, obj3] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); + // obj2 and obj3 are not retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // obj2 and obj3 are excluded from the results + ]); + }); + + it('handles circular references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj1] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // obj1 is retrieved once, and it is not retrieved again in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj1 reflects the inbound reference to itself + ]); + }); + + it('handles a reference graph more than 20 layers deep (circuit-breaker)', async () => { + const type = MULTI_NAMESPACE_OBJ_TYPE_1; + const params = setup([{ type, id: 'id-1' }]); + for (let i = 1; i < 100; i++) { + mockMgetResults({ found: true, references: [{ type, id: `id-${i + 1}` }] }); + } + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + /Exceeded maximum reference graph depth/ + ); + expect(params.client.mget).toHaveBeenCalledTimes(20); + }); + + it('handles multiple inbound references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1, obj2]); + mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [obj3] }); // results for obj1 and obj2 + mockMgetResults({ found: true }); // results for obj3 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2); + expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: SPACES, inboundReferences: [] }, + { + ...obj3, + spaces: SPACES, + inboundReferences: [ + // obj3 reflects both inbound references + { ...obj1, name: 'ref-name' }, + { ...obj2, name: 'ref-name' }, + ], + }, + ]); + }); + + it('handles transitive references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj2] }); // results for obj1 + mockMgetResults({ found: true, references: [obj3] }); // results for obj2 + mockMgetResults({ found: true }); // results for obj3 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(3); + expectMgetArgs(1, obj1); + expectMgetArgs(2, obj2); // obj2 is retrieved in a second cluster call + expectMgetArgs(3, obj3); // obj3 is retrieved in a third cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj2 reflects the inbound reference + { ...obj3, spaces: SPACES, inboundReferences: [{ ...obj2, name: 'ref-name' }] }, // obj3 reflects the inbound reference + ]); + }); + + it('handles missing objects and missing references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; // found, with missing references to obj4 and obj5 + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; // missing object (found, but doesn't exist in the current space)) + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; // missing object (not found + const obj4 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-4' }; // missing reference (found but doesn't exist in the current space) + const obj5 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-5' }; // missing reference (not found) + const params = setup([obj1, obj2, obj3]); + mockMgetResults({ found: true, references: [obj4, obj5] }, { found: true }, { found: false }); // results for obj1, obj2, and obj3 + mockMgetResults({ found: true }, { found: false }); // results for obj4 and obj5 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj1 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj2 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2, obj3); + expectMgetArgs(2, obj4, obj5); + expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3); + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: [], inboundReferences: [], isMissing: true }, + { ...obj3, spaces: [], inboundReferences: [], isMissing: true }, + { ...obj4, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true }, + { ...obj5, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true }, + ]); + }); + + it('handles the purpose="updateObjectsSpaces" option', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-3' }; + const params = setup([obj1, obj2], { purpose: 'updateObjectsSpaces' }); + mockMgetResults({ found: true, references: [obj3] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // obj2 is excluded + // obj3 is not retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // even though it is excluded from the cluster call, obj2 is included in the results + { ...obj2, spaces: [], inboundReferences: [] }, + // obj3 is excluded from the results + ]); + }); + + describe('legacy URL aliases', () => { + it('uses the PointInTimeFinder to search for legacy URL aliases', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1, obj2], {}); + mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [] }); // results for obj1 and obj2 + mockMgetResults({ found: true, references: [] }); // results for obj3 + mockFindResults( + // mock search results for four aliases for obj1, and none for obj2 or obj3 + ...[1, 2, 3, 4].map((i) => ({ + sourceId: obj1.id, + targetId: 'doesnt-matter', + targetType: obj1.type, + targetNamespace: `space-${i}`, + })) + ); + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2); + expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments; + expect(kueryFilterArgs).toHaveLength(2); + const typeAndIdFilters = kueryFilterArgs[1].arguments; + expect(typeAndIdFilters).toHaveLength(3); + [obj1, obj2, obj3].forEach(({ type, id }, i) => { + const typeAndIdFilter = typeAndIdFilters[i].arguments; + expect(typeAndIdFilter).toEqual([ + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: type }]), + }), + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: id }]), + }), + ]); + }); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + expect(result.objects).toEqual([ + { + ...obj1, + spaces: SPACES, + inboundReferences: [], + spacesWithMatchingAliases: ['space-1', 'space-2', 'space-3', 'space-4'], + }, + { ...obj2, spaces: SPACES, inboundReferences: [] }, + { ...obj3, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, + ]); + }); + + it('does not create a PointInTimeFinder if no objects are passed in', async () => { + const params = setup([]); + + await collectMultiNamespaceReferences(params); + expect(params.createPointInTimeFinder).not.toHaveBeenCalled(); + }); + + it('does not search for objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const params = setup([obj1, obj2]); + mockMgetResults({ found: true }, { found: false }); // results for obj1 and obj2 + + await collectMultiNamespaceReferences(params); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + + const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments; + expect(kueryFilterArgs).toHaveLength(2); + const typeAndIdFilters = kueryFilterArgs[1].arguments; + expect(typeAndIdFilters).toHaveLength(1); + const typeAndIdFilter = typeAndIdFilters[0].arguments; + expect(typeAndIdFilter).toEqual([ + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: obj1.type }]), + }), + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: obj1.id }]), + }), + ]); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + }); + + it('does not search at all if all objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: false }); // results for obj1 + + await collectMultiNamespaceReferences(params); + expect(params.createPointInTimeFinder).not.toHaveBeenCalled(); + }); + + it('handles PointInTimeFinder.find errors', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true }); // results for obj1 + savedObjectsMock.find.mockRejectedValue(new Error('Oh no!')); + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + 'Failed to retrieve legacy URL aliases: Oh no!' + ); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); // we still close the point-in-time, even though the search failed + }); + + it('handles PointInTimeFinder.close errors', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true }); // results for obj1 + savedObjectsMock.closePointInTime.mockRejectedValue(new Error('Oh no!')); + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + 'Failed to retrieve legacy URL aliases: Oh no!' + ); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts new file mode 100644 index 000000000000000..43923695f6548ec --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts @@ -0,0 +1,310 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// @ts-expect-error no ts +import { esKuery } from '../../es_query'; + +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsSerializer } from '../../serialization'; +import type { SavedObject, SavedObjectsBaseOptions } from '../../types'; +import { getRootFields } from './included_fields'; +import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; +import type { + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, +} from './point_in_time_finder'; +import type { RepositoryEsClient } from './repository_es_client'; + +/** + * When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error. + */ +const MAX_REFERENCE_GRAPH_DEPTH = 20; + +/** + * How many aliases to search for per page. This is smaller than the PointInTimeFinder's default of 1000. We specify 100 for the page count + * because this is a relatively unimportant operation, and we want to avoid blocking the Elasticsearch thread pool for longer than + * necessary. + */ +const ALIAS_SEARCH_PER_PAGE = 100; + +/** + * An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the + * `namespaceType: 'multiple'` or `namespaceType: 'multiple-isolated'` option). + * + * Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with + * the `namespaceType: 'multiple'`). + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesObject { + id: string; + type: string; +} + +/** + * Options for collecting references. + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesOptions + extends SavedObjectsBaseOptions { + /** Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' */ + purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +} + +/** + * A returned input object or one of its references, with additional context. + * + * @public + */ +export interface SavedObjectReferenceWithContext { + /** The type of the referenced object */ + type: string; + /** The ID of the referenced object */ + id: string; + /** The space(s) that the referenced object exists in */ + spaces: string[]; + /** + * References to this object; note that this does not contain _all inbound references everywhere for this object_, it only contains + * inbound references for the scope of this operation + */ + inboundReferences: Array<{ + /** The type of the object that has the inbound reference */ + type: string; + /** The ID of the object that has the inbound reference */ + id: string; + /** The name of the inbound reference */ + name: string; + }>; + /** Whether or not this object or reference is missing */ + isMissing?: boolean; + /** The space(s) that legacy URL aliases matching this type/id exist in */ + spacesWithMatchingAliases?: string[]; +} + +/** + * The response when object references are collected. + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + objects: SavedObjectReferenceWithContext[]; +} + +/** + * Parameters for the collectMultiNamespaceReferences function. + * + * @internal + */ +export interface CollectMultiNamespaceReferencesParams { + registry: ISavedObjectTypeRegistry; + allowedTypes: string[]; + client: RepositoryEsClient; + serializer: SavedObjectsSerializer; + getIndexForType: (type: string) => string; + createPointInTimeFinder: ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions + ) => ISavedObjectsPointInTimeFinder; + objects: SavedObjectsCollectMultiNamespaceReferencesObject[]; + options?: SavedObjectsCollectMultiNamespaceReferencesOptions; +} + +/** + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + */ +export async function collectMultiNamespaceReferences( + params: CollectMultiNamespaceReferencesParams +): Promise { + const { createPointInTimeFinder, objects } = params; + if (!objects.length) { + return { objects: [] }; + } + + const { objectMap, inboundReferencesMap } = await getObjectsAndReferences(params); + const objectsWithContext = Array.from( + inboundReferencesMap.entries() + ).map(([referenceKey, referenceVal]) => { + const inboundReferences = Array.from(referenceVal.entries()).map(([objectKey, name]) => { + const { type, id } = parseKey(objectKey); + return { type, id, name }; + }); + const { type, id } = parseKey(referenceKey); + const object = objectMap.get(referenceKey); + const spaces = object?.namespaces ?? []; + return { type, id, spaces, inboundReferences, ...(object === null && { isMissing: true }) }; + }); + + const aliasesMap = await checkLegacyUrlAliases(createPointInTimeFinder, objectsWithContext); + const results = objectsWithContext.map((obj) => { + const key = getKey(obj); + const val = aliasesMap.get(key); + const spacesWithMatchingAliases = val && Array.from(val); + return { ...obj, spacesWithMatchingAliases }; + }); + + return { + objects: results, + }; +} + +/** + * Recursively fetches objects and their references, returning a map of the retrieved objects and a map of all inbound references. + */ +async function getObjectsAndReferences({ + registry, + allowedTypes, + client, + serializer, + getIndexForType, + objects, + options = {}, +}: CollectMultiNamespaceReferencesParams) { + const { namespace, purpose } = options; + const inboundReferencesMap = objects.reduce( + // Add the input objects to the references map so they are returned with the results, even if they have no inbound references + (acc, cur) => acc.set(getKey(cur), new Map()), + new Map>() + ); + const objectMap = new Map(); + + const rootFields = getRootFields(); + const makeBulkGetDocs = (objectsToGet: SavedObjectsCollectMultiNamespaceReferencesObject[]) => + objectsToGet.map(({ type, id }) => ({ + _id: serializer.generateRawId(undefined, type, id), + _index: getIndexForType(type), + _source: rootFields, // Optimized to only retrieve root fields (ignoring type-specific fields) + })); + const validObjectTypesFilter = ({ type }: SavedObjectsCollectMultiNamespaceReferencesObject) => + allowedTypes.includes(type) && + (purpose === 'updateObjectsSpaces' + ? registry.isShareable(type) + : registry.isMultiNamespace(type)); + + let bulkGetObjects = objects.filter(validObjectTypesFilter); + let count = 0; // this is a circuit-breaker to ensure we don't hog too many resources; we should never have an object graph this deep + while (bulkGetObjects.length) { + if (count >= MAX_REFERENCE_GRAPH_DEPTH) { + throw new Error( + `Exceeded maximum reference graph depth of ${MAX_REFERENCE_GRAPH_DEPTH} objects!` + ); + } + const bulkGetResponse = await client.mget( + { body: { docs: makeBulkGetDocs(bulkGetObjects) } }, + { ignore: [404] } + ); + const newObjectsToGet = new Set(); + for (let i = 0; i < bulkGetObjects.length; i++) { + // For every element in bulkGetObjects, there should be a matching element in bulkGetResponse.body.docs + const { type, id } = bulkGetObjects[i]; + const objectKey = getKey({ type, id }); + const doc = bulkGetResponse.body.docs[i]; + // @ts-expect-error MultiGetHit._source is optional + if (!doc.found || !rawDocExistsInNamespace(registry, doc, namespace)) { + objectMap.set(objectKey, null); + continue; + } + // @ts-expect-error MultiGetHit._source is optional + const object = getSavedObjectFromSource(registry, type, id, doc); + objectMap.set(objectKey, object); + for (const reference of object.references) { + if (!validObjectTypesFilter(reference)) { + continue; + } + const referenceKey = getKey(reference); + const referenceVal = inboundReferencesMap.get(referenceKey) ?? new Map(); + if (!referenceVal.has(objectKey)) { + inboundReferencesMap.set(referenceKey, referenceVal.set(objectKey, reference.name)); + } + if (!objectMap.has(referenceKey)) { + newObjectsToGet.add(referenceKey); + } + } + } + bulkGetObjects = Array.from(newObjectsToGet).map((key) => parseKey(key)); + count++; + } + + return { objectMap, inboundReferencesMap }; +} + +/** + * Fetches all legacy URL aliases that match the given objects, returning a map of the matching aliases and what space(s) they exist in. + */ +async function checkLegacyUrlAliases( + createPointInTimeFinder: ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions + ) => ISavedObjectsPointInTimeFinder, + objects: SavedObjectReferenceWithContext[] +) { + const filteredObjects = objects.filter(({ spaces }) => spaces.length !== 0); + if (!filteredObjects.length) { + return new Map>(); + } + const filter = createAliasKueryFilter(filteredObjects); + const finder = createPointInTimeFinder({ + type: LEGACY_URL_ALIAS_TYPE, + perPage: ALIAS_SEARCH_PER_PAGE, + filter, + }); + const aliasesMap = new Map>(); + let error: Error | undefined; + try { + for await (const { saved_objects: savedObjects } of finder.find()) { + for (const alias of savedObjects) { + const { sourceId, targetType, targetNamespace } = alias.attributes; + const key = getKey({ type: targetType, id: sourceId }); + const val = aliasesMap.get(key) ?? new Set(); + val.add(targetNamespace); + aliasesMap.set(key, val); + } + } + } catch (e) { + error = e; + } + + try { + await finder.close(); + } catch (e) { + if (!error) { + error = e; + } + } + + if (error) { + throw new Error(`Failed to retrieve legacy URL aliases: ${error.message}`); + } + return aliasesMap; +} + +function createAliasKueryFilter(objects: SavedObjectReferenceWithContext[]) { + const { buildNode } = esKuery.nodeTypes.function; + const kueryNodes = objects.reduce((acc, { type, id }) => { + const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.targetType`, type); + const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.sourceId`, id); + acc.push(buildNode('and', [match1, match2])); + return acc; + }, []); + return buildNode('and', [ + buildNode('not', buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.disabled`, true)), // ignore aliases that have been disabled + buildNode('or', kueryNodes), + ]); +} + +/** Takes an object with a `type` and `id` field and returns a key string */ +function getKey({ type, id }: { type: string; id: string }) { + return `${type}:${id}`; +} + +/** Parses a 'type:id' key string and returns an object with a `type` field and an `id` field */ +function parseKey(key: string) { + const type = key.slice(0, key.indexOf(':')); + const id = key.slice(type.length + 1); + return { type, id }; +} diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index 334cda91129f3e9..51c431b1c6b3b0b 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -6,125 +6,63 @@ * Side Public License, v 1. */ -import { includedFields } from './included_fields'; +import { getRootFields, includedFields } from './included_fields'; -const BASE_FIELD_COUNT = 10; +describe('getRootFields', () => { + it('returns copy of root fields', () => { + const fields = getRootFields(); + expect(fields).toMatchInlineSnapshot(` + Array [ + "namespace", + "namespaces", + "type", + "references", + "migrationVersion", + "coreMigrationVersion", + "updated_at", + "originId", + ] + `); + }); +}); describe('includedFields', () => { + const rootFields = getRootFields(); + it('returns undefined if fields are not provided', () => { expect(includedFields()).toBe(undefined); }); - it('accepts type string', () => { + it('accepts type and field as string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('type'); + expect(fields).toEqual(['config.foo', ...rootFields, 'foo']); }); - it('accepts type as string array', () => { + it('accepts type as array and field as string', () => { const fields = includedFields(['config', 'secret'], 'foo'); - expect(fields).toMatchInlineSnapshot(` -Array [ - "config.foo", - "secret.foo", - "namespace", - "namespaces", - "type", - "references", - "migrationVersion", - "coreMigrationVersion", - "updated_at", - "originId", - "foo", -] -`); - }); - - it('accepts field as string', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('config.foo'); + expect(fields).toEqual(['config.foo', 'secret.foo', ...rootFields, 'foo']); }); - it('accepts fields as an array', () => { + it('accepts type as string and field as array', () => { const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); - expect(fields).toContain('config.foo'); - expect(fields).toContain('config.bar'); + expect(fields).toEqual(['config.foo', 'config.bar', ...rootFields, 'foo', 'bar']); }); - it('accepts type as string array and fields as string array', () => { + it('accepts type as array and field as array', () => { const fields = includedFields(['config', 'secret'], ['foo', 'bar']); - expect(fields).toMatchInlineSnapshot(` -Array [ - "config.foo", - "config.bar", - "secret.foo", - "secret.bar", - "namespace", - "namespaces", - "type", - "references", - "migrationVersion", - "coreMigrationVersion", - "updated_at", - "originId", - "foo", - "bar", -] -`); - }); - - it('includes namespace', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('namespace'); - }); - - it('includes namespaces', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('namespaces'); - }); - - it('includes references', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('references'); - }); - - it('includes migrationVersion', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('migrationVersion'); - }); - - it('includes updated_at', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('updated_at'); - }); - - it('includes originId', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('originId'); + expect(fields).toEqual([ + 'config.foo', + 'config.bar', + 'secret.foo', + 'secret.bar', + ...rootFields, + 'foo', + 'bar', + ]); }); it('uses wildcard when type is not provided', () => { const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('*.foo'); - }); - - describe('v5 compatibility', () => { - it('includes legacy field path', () => { - const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); - expect(fields).toContain('foo'); - expect(fields).toContain('bar'); - }); + expect(fields).toEqual(['*.foo', ...rootFields, 'foo']); }); }); diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index cef83f103ec5391..9613d8f6a4a41e7 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -9,6 +9,22 @@ function toArray(value: string | string[]): string[] { return typeof value === 'string' ? [value] : value; } + +const ROOT_FIELDS = [ + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'coreMigrationVersion', + 'updated_at', + 'originId', +]; + +export function getRootFields() { + return [...ROOT_FIELDS]; +} + /** * Provides an array of paths for ES source filtering */ @@ -28,13 +44,6 @@ export function includedFields( .reduce((acc: string[], t) => { return [...acc, ...sourceFields.map((f) => `${t}.${f}`)]; }, []) - .concat('namespace') - .concat('namespaces') - .concat('type') - .concat('references') - .concat('migrationVersion') - .concat('coreMigrationVersion') - .concat('updated_at') - .concat('originId') + .concat(ROOT_FIELDS) .concat(fields); // v5 compatibility } diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 09bce81b14c39f6..661d04b8a0b2a02 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -27,3 +27,17 @@ export type { export { SavedObjectsErrorHelpers } from './errors'; export { SavedObjectsUtils } from './utils'; + +export type { + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from './collect_multi_namespace_references'; + +export type { + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, +} from './update_objects_spaces'; diff --git a/src/core/server/saved_objects/service/lib/internal_utils.test.ts b/src/core/server/saved_objects/service/lib/internal_utils.test.ts new file mode 100644 index 000000000000000..d1fd067990f07b7 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/internal_utils.test.ts @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import type { SavedObjectsRawDoc } from '../../serialization'; +import { encodeHitVersion } from '../../version'; +import { + getBulkOperationError, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from './internal_utils'; +import { ALL_NAMESPACES_STRING } from './utils'; + +describe('#getBulkOperationError', () => { + const type = 'obj-type'; + const id = 'obj-id'; + + it('returns index not found error', () => { + const rawResponse = { + status: 404, + error: { type: 'index_not_found_exception', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred', // TODO: this error payload is not very helpful to consumers, can we change it? + statusCode: 500, + }); + }); + + it('returns generic not found error', () => { + const rawResponse = { + status: 404, + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Not Found', + message: `Saved object [${type}/${id}] not found`, + statusCode: 404, + }); + }); + + it('returns conflict error', () => { + const rawResponse = { + status: 409, + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Conflict', + message: `Saved object [${type}/${id}] conflict`, + statusCode: 409, + }); + }); + + it('returns an unexpected result error', () => { + const rawResponse = { + status: 123, // any status + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Internal Server Error', + message: `Unexpected bulk response [${rawResponse.status}] ${rawResponse.error.type}: ${rawResponse.error.reason}`, + statusCode: 500, + }); + }); +}); + +describe('#getSavedObjectFromSource', () => { + const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type'; + const NON_NAMESPACE_AGNOSTIC_TYPE = 'other-type'; + + const registry = typeRegistryMock.create(); + registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE); + + const id = 'obj-id'; + const _seq_no = 1; + const _primary_term = 1; + const attributes = { foo: 'bar' }; + const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }]; + const migrationVersion = { foo: 'migrationVersion' }; + const coreMigrationVersion = 'coreMigrationVersion'; + const originId = 'originId'; + // eslint-disable-next-line @typescript-eslint/naming-convention + const updated_at = 'updatedAt'; + + function createRawDoc( + type: string, + namespaceAttrs?: { namespace?: string; namespaces?: string[] } + ) { + return { + // other fields exist on the raw document, but they are not relevant to these test cases + _seq_no, + _primary_term, + _source: { + type, + [type]: attributes, + references, + migrationVersion, + coreMigrationVersion, + originId, + updated_at, + ...namespaceAttrs, + }, + }; + } + + it('returns object with expected attributes', () => { + const type = 'any-type'; + const doc = createRawDoc(type); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual({ + attributes, + coreMigrationVersion, + id, + migrationVersion, + namespaces: expect.anything(), // see specific test cases below + originId, + references, + type, + updated_at, + version: encodeHitVersion(doc), + }); + }); + + it('returns object with empty namespaces array when type is namespace-agnostic', () => { + const type = NAMESPACE_AGNOSTIC_TYPE; + const doc = createRawDoc(type); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual(expect.objectContaining({ namespaces: [] })); + }); + + it('returns object with namespaces when type is not namespace-agnostic and namespaces array is defined', () => { + const type = NON_NAMESPACE_AGNOSTIC_TYPE; + const namespaces = ['foo-ns', 'bar-ns']; + const doc = createRawDoc(type, { namespaces }); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual(expect.objectContaining({ namespaces })); + }); + + it('derives namespaces from namespace attribute when type is not namespace-agnostic and namespaces array is not defined', () => { + // Deriving namespaces from the namespace attribute is an implementation detail of SavedObjectsUtils.namespaceIdToString(). + // However, these test cases assertions are written out anyway for clarity. + const type = NON_NAMESPACE_AGNOSTIC_TYPE; + const doc1 = createRawDoc(type, { namespace: undefined }); + const doc2 = createRawDoc(type, { namespace: 'foo-ns' }); + + const result1 = getSavedObjectFromSource(registry, type, id, doc1); + const result2 = getSavedObjectFromSource(registry, type, id, doc2); + expect(result1).toEqual(expect.objectContaining({ namespaces: ['default'] })); + expect(result2).toEqual(expect.objectContaining({ namespaces: ['foo-ns'] })); + }); +}); + +describe('#rawDocExistsInNamespace', () => { + const SINGLE_NAMESPACE_TYPE = 'single-type'; + const MULTI_NAMESPACE_TYPE = 'multi-type'; + const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type'; + + const registry = typeRegistryMock.create(); + registry.isSingleNamespace.mockImplementation((type) => type === SINGLE_NAMESPACE_TYPE); + registry.isMultiNamespace.mockImplementation((type) => type === MULTI_NAMESPACE_TYPE); + registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE); + + function createRawDoc( + type: string, + namespaceAttrs: { namespace?: string; namespaces?: string[] } + ) { + return { + // other fields exist on the raw document, but they are not relevant to these test cases + _source: { + type, + ...namespaceAttrs, + }, + } as SavedObjectsRawDoc; + } + + describe('single-namespace type', () => { + it('returns true regardless of namespace or namespaces fields', () => { + // Technically, a single-namespace type does not exist in a space unless it has a namespace prefix in its raw ID and a matching + // 'namespace' field. However, historically we have not enforced the latter, we have just relied on searching for and deserializing + // documents with the correct namespace prefix. We may revisit this in the future. + const doc1 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespace: 'some-space' }); // the namespace field is ignored + const doc2 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored + expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true); + }); + }); + + describe('multi-namespace type', () => { + const docInDefaultSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['default'] }); + const docInSomeSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['some-space'] }); + const docInAllSpaces = createRawDoc(MULTI_NAMESPACE_TYPE, { + namespaces: [ALL_NAMESPACES_STRING], + }); + const docInNoSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: [] }); + + it('returns true when the document namespaces matches', () => { + expect(rawDocExistsInNamespace(registry, docInDefaultSpace, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'other-space')).toBe(true); + }); + + it('returns false when the document namespace does not match', () => { + expect(rawDocExistsInNamespace(registry, docInDefaultSpace, 'other-space')).toBe(false); + expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'other-space')).toBe(false); + expect(rawDocExistsInNamespace(registry, docInNoSpace, 'other-space')).toBe(false); + }); + }); + + describe('namespace-agnostic type', () => { + it('returns true regardless of namespace or namespaces fields', () => { + const doc1 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespace: 'some-space' }); // the namespace field is ignored + const doc2 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored + expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/internal_utils.ts b/src/core/server/saved_objects/service/lib/internal_utils.ts new file mode 100644 index 000000000000000..feaaea15649c766 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/internal_utils.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Payload } from '@hapi/boom'; +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; +import type { SavedObject } from '../../types'; +import { decodeRequestVersion, encodeHitVersion } from '../../version'; +import { SavedObjectsErrorHelpers } from './errors'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from './utils'; + +/** + * Checks the raw response of a bulk operation and returns an error if necessary. + * + * @param type + * @param id + * @param rawResponse + * + * @internal + */ +export function getBulkOperationError( + type: string, + id: string, + rawResponse: { + status: number; + error?: { type: string; reason: string; index: string }; + // Other fields are present on a bulk operation result but they are irrelevant for this function + } +): Payload | undefined { + const { status, error } = rawResponse; + if (error) { + switch (status) { + case 404: + return error.type === 'index_not_found_exception' + ? SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index).output.payload + : SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload; + case 409: + return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + default: + return { + error: 'Internal Server Error', + message: `Unexpected bulk response [${status}] ${error.type}: ${error.reason}`, + statusCode: 500, + }; + } + } +} + +/** + * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. + * + * @param version Optional version specified by the consumer. + * @param document Optional existing document that was obtained in a preflight operation. + * + * @internal + */ +export function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { + if (version) { + return decodeRequestVersion(version); + } else if (document) { + return { + if_seq_no: document._seq_no, + if_primary_term: document._primary_term, + }; + } + return {}; +} + +/** + * Gets a saved object from a raw ES document. + * + * @param registry + * @param type + * @param id + * @param doc + * + * @internal + */ +export function getSavedObjectFromSource( + registry: ISavedObjectTypeRegistry, + type: string, + id: string, + doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } +): SavedObject { + const { originId, updated_at: updatedAt } = doc._source; + + let namespaces: string[] = []; + if (!registry.isNamespaceAgnostic(type)) { + namespaces = doc._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(doc._source.namespace), + ]; + } + + return { + id, + type, + namespaces, + ...(originId && { originId }), + ...(updatedAt && { updated_at: updatedAt }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + coreMigrationVersion: doc._source.coreMigrationVersion, + }; +} + +/** + * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as + * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the + * document's `namespaces` value includes the string representation of the given namespace. + * + * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID + * format mentioned above do not apply. + * + * @param registry + * @param raw + * @param namespace + */ +export function rawDocExistsInNamespace( + registry: ISavedObjectTypeRegistry, + raw: SavedObjectsRawDoc, + namespace: string | undefined +) { + const rawDocType = raw._source.type; + + // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees + // of the document ID format and don't need to check this + if (!registry.isMultiNamespace(rawDocType)) { + return true; + } + + const namespaces = raw._source.namespaces; + const existsInNamespace = + namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) || + namespaces?.includes(ALL_NAMESPACES_STRING); + return existsInNamespace ?? false; +} diff --git a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts index 9a8dcceafebb285..f0ed943c585e58b 100644 --- a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts @@ -39,14 +39,14 @@ export interface PointInTimeFinderDependencies } /** @public */ -export interface ISavedObjectsPointInTimeFinder { +export interface ISavedObjectsPointInTimeFinder { /** * An async generator which wraps calls to `savedObjectsClient.find` and * iterates over multiple pages of results using `_pit` and `search_after`. * This will open a new Point-In-Time (PIT), and continue paging until a set * of results is received that's smaller than the designated `perPage` size. */ - find: () => AsyncGenerator; + find: () => AsyncGenerator>; /** * Closes the Point-In-Time associated with this finder instance. * @@ -63,7 +63,8 @@ export interface ISavedObjectsPointInTimeFinder { /** * @internal */ -export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { +export class PointInTimeFinder + implements ISavedObjectsPointInTimeFinder { readonly #log: Logger; readonly #client: PointInTimeFinderClient; readonly #findOptions: SavedObjectsFindOptions; @@ -162,7 +163,7 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { searchAfter?: estypes.Id[]; }) { try { - return await this.#client.find({ + return await this.#client.find({ // Sort fields are required to use searchAfter, so we set some defaults here sortField: 'updated_at', sortOrder: 'desc', diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index a2092e057180859..0e1426a58f8ae17 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -24,11 +24,11 @@ const create = () => { openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), deleteByNamespace: jest.fn(), incrementCounter: jest.fn(), removeReferencesTo: jest.fn(), + collectMultiNamespaceReferences: jest.fn(), + updateObjectsSpaces: jest.fn(), }; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ 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 33754d0ad966185..22c40a547f419a2 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { pointInTimeFinderMock } from './repository.test.mock'; +import { + pointInTimeFinderMock, + mockCollectMultiNamespaceReferences, + mockGetBulkOperationError, + mockUpdateObjectsSpaces, +} from './repository.test.mock'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; @@ -67,9 +72,9 @@ describe('SavedObjectsRepository', () => { * This type has namespaceType: 'multiple-isolated'. * * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is NOT shareable - * across namespaces. This distinction only matters when using the `addToNamespaces` and `deleteFromNamespaces` APIs, or when using the - * `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what namespaces an object - * exists in. + * across namespaces. This distinction only matters when using the `collectMultiNamespaceReferences` or `updateObjectsSpaces` APIs, or + * when using the `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what + * namespaces an object exists in. * * In a nutshell, this type is more restrictive than `MULTI_NAMESPACE_TYPE`, so we use `MULTI_NAMESPACE_ISOLATED_TYPE` for any test cases * where `MULTI_NAMESPACE_TYPE` would also satisfy the test case. @@ -295,164 +300,6 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'search_0', type: 'search', id: '123' }], }); - describe('#addToNamespaces', () => { - const id = 'some-id'; - const type = MULTI_NAMESPACE_TYPE; - const currentNs1 = 'default'; - const currentNs2 = 'foo-namespace'; - const newNs1 = 'bar-namespace'; - const newNs2 = 'baz-namespace'; - - const mockGetResponse = (type, id) => { - // mock a document that exists in two namespaces - const mockResponse = getMockGetResponse({ type, id }); - mockResponse._source.namespaces = [currentNs1, currentNs2]; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - }; - - const addToNamespacesSuccess = async (type, id, namespaces, options) => { - mockGetResponse(type, id); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }) - ); - const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use ES get action then update action`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - }); - - it(`defaults to the version of the existing document`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), - expect.anything() - ); - }); - - it(`accepts version`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2], { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, namespaces, options) => { - await expect( - savedObjectsRepository.addToNamespaces(type, id, namespaces, options) - ).rejects.toThrowError(createGenericNotFoundError(type, id)); - }; - const expectBadRequestError = async (type, id, namespaces, message) => { - await expect( - savedObjectsRepository.addToNamespaces(type, id, namespaces) - ).rejects.toThrowError(createBadRequestError(message)); - }; - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id, [newNs1, newNs2]); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is not shareable`, async () => { - const test = async (type) => { - const message = `${type} doesn't support multiple namespaces`; - await expectBadRequestError(type, id, [newNs1, newNs2], message); - expect(client.update).not.toHaveBeenCalled(); - }; - await test('index-pattern'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); - }); - - it(`throws when namespaces is an empty array`, async () => { - const test = async (namespaces) => { - const message = 'namespaces must be a non-empty array of strings'; - await expectBadRequestError(type, id, namespaces, message); - expect(client.update).not.toHaveBeenCalled(); - }; - await test([]); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id); - await expectNotFoundError(type, id, [newNs1, newNs2], { - namespace: 'some-other-namespace', - }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id); - client.update.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns all existing and new namespaces on success`, async () => { - const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expect(result).toEqual({ namespaces: [currentNs1, currentNs2, newNs1, newNs2] }); - }); - - it(`succeeds when adding existing namespaces`, async () => { - const result = await addToNamespacesSuccess(type, id, [currentNs1]); - expect(result).toEqual({ namespaces: [currentNs1, currentNs2] }); - }); - }); - }); - describe('#bulkCreate', () => { const obj1 = { type: 'config', @@ -757,6 +604,10 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + const obj3 = { type: 'dashboard', id: 'three', @@ -764,11 +615,13 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'ref_0', type: 'test', id: '2' }], }; - const bulkCreateError = async (obj, esError, expectedError) => { + const bulkCreateError = async (obj, isBulkError, expectedErrorResult) => { let response; - if (esError) { + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); response = getMockBulkCreateResponse([obj1, obj, obj2]); - response.items[1].create = { error: esError }; } else { response = getMockBulkCreateResponse([obj1, obj2]); } @@ -779,14 +632,14 @@ describe('SavedObjectsRepository', () => { const objects = [obj1, obj, obj2]; const result = await savedObjectsRepository.bulkCreate(objects); expect(client.bulk).toHaveBeenCalled(); - const objCall = esError ? expectObjArgs(obj) : []; + const objCall = isBulkError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), expect.anything() ); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], }); }; @@ -878,25 +731,9 @@ describe('SavedObjectsRepository', () => { }); }); - it(`returns error when there is a version conflict (bulk)`, async () => { - const esError = { type: 'version_conflict_engine_exception' }; - await bulkCreateError(obj3, esError, expectErrorConflict(obj3)); - }); - - it(`returns error when document is missing`, async () => { - const esError = { type: 'document_missing_exception' }; - await bulkCreateError(obj3, esError, expectErrorNotFound(obj3)); - }); - - it(`returns error reason for other errors`, async () => { - const esError = { reason: 'some_other_error' }; - await bulkCreateError(obj3, esError, expectErrorResult(obj3, { message: esError.reason })); - }); - - it(`returns error string for other errors if no reason is defined`, async () => { - const esError = { foo: 'some_other_error' }; - const expectedError = expectErrorResult(obj3, { message: JSON.stringify(esError) }); - await bulkCreateError(obj3, esError, expectedError); + it(`returns bulk error`, async () => { + const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' }; + await bulkCreateError(obj3, true, expectedErrorResult); }); }); @@ -1530,16 +1367,22 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + const obj = { type: 'dashboard', id: 'three', }; - const bulkUpdateError = async (obj, esError, expectedError) => { + const bulkUpdateError = async (obj, isBulkError, expectedErrorResult) => { const objects = [obj1, obj, obj2]; const mockResponse = getMockBulkUpdateResponse(objects); - if (esError) { - mockResponse.items[1].update = { error: esError }; + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); } client.bulk.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) @@ -1547,14 +1390,14 @@ describe('SavedObjectsRepository', () => { const result = await savedObjectsRepository.bulkUpdate(objects); expect(client.bulk).toHaveBeenCalled(); - const objCall = esError ? expectObjArgs(obj) : []; + const objCall = isBulkError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), expect.anything() ); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], }); }; @@ -1592,19 +1435,19 @@ describe('SavedObjectsRepository', () => { it(`returns error when type is invalid`, async () => { const _obj = { ...obj, type: 'unknownType' }; - await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); }); it(`returns error when type is hidden`, async () => { const _obj = { ...obj, type: HIDDEN_TYPE }; - await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); }); it(`returns error when object namespace is '*'`, async () => { const _obj = { ...obj, namespace: '*' }; await bulkUpdateError( _obj, - undefined, + false, expectErrorResult(obj, createBadRequestError('"namespace" cannot be "*"')) ); }); @@ -1627,25 +1470,9 @@ describe('SavedObjectsRepository', () => { await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); }); - it(`returns error when there is a version conflict (bulk)`, async () => { - const esError = { type: 'version_conflict_engine_exception' }; - await bulkUpdateError(obj, esError, expectErrorConflict(obj)); - }); - - it(`returns error when document is missing (bulk)`, async () => { - const esError = { type: 'document_missing_exception' }; - await bulkUpdateError(obj, esError, expectErrorNotFound(obj)); - }); - - it(`returns error reason for other errors (bulk)`, async () => { - const esError = { reason: 'some_other_error' }; - await bulkUpdateError(obj, esError, expectErrorResult(obj, { message: esError.reason })); - }); - - it(`returns error string for other errors if no reason is defined (bulk)`, async () => { - const esError = { foo: 'some_other_error' }; - const expectedError = expectErrorResult(obj, { message: JSON.stringify(esError) }); - await bulkUpdateError(obj, esError, expectedError); + it(`returns bulk error`, async () => { + const expectedErrorResult = { type: obj.type, id: obj.id, error: 'Oh no, a bulk error!' }; + await bulkUpdateError(obj, true, expectedErrorResult); }); }); @@ -3898,352 +3725,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('#deleteFromNamespaces', () => { - const id = 'some-id'; - const type = MULTI_NAMESPACE_TYPE; - const namespace1 = 'default'; - const namespace2 = 'foo-namespace'; - const namespace3 = 'bar-namespace'; - - const mockGetResponse = (type, id, namespaces) => { - // mock a document that exists in two namespaces - const mockResponse = getMockGetResponse({ type, id }); - mockResponse._source.namespaces = namespaces; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - }; - - const deleteFromNamespacesSuccess = async ( - type, - id, - namespaces, - currentNamespaces, - options - ) => { - mockGetResponse(type, id, currentNamespaces); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'deleted', - }) - ); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }) - ); - - return await savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options); - }; - - describe('client calls', () => { - describe('delete action', () => { - const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => { - const test = async (namespaces) => { - await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options); - expectFn(); - client.delete.mockClear(); - client.get.mockClear(); - }; - await test([namespace1]); - await test([namespace1, namespace2]); - }; - - it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => { - const expectFn = () => { - expect(client.delete).toHaveBeenCalledTimes(1); - expect(client.get).toHaveBeenCalledTimes(1); - }; - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`formats the ES requests`, async () => { - const expectFn = () => { - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - ...versionProperties, - }), - expect.anything() - ); - }; - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await deleteFromNamespacesSuccessDelete(() => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ) - ); - }); - - it(`should use default index`, async () => { - const expectFn = () => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test' }), - expect.anything() - ); - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`should use custom index`, async () => { - const expectFn = () => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom' }), - expect.anything() - ); - await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); - }); - }); - - describe('update action', () => { - const deleteFromNamespacesSuccessUpdate = async (expectFn, options, _type = type) => { - const test = async (remaining) => { - const currentNamespaces = [namespace1].concat(remaining); - await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options); - expectFn(); - client.get.mockClear(); - client.update.mockClear(); - }; - await test([namespace2]); - await test([namespace2, namespace3]); - }; - - it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => { - const expectFn = () => { - expect(client.update).toHaveBeenCalledTimes(1); - expect(client.get).toHaveBeenCalledTimes(1); - }; - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`formats the ES requests`, async () => { - let ctr = 0; - const expectFn = () => { - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3]; - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - ...versionProperties, - body: { doc: { ...mockTimestampFields, namespaces } }, - }), - expect.anything() - ); - }; - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`should use default index`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test' }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`should use custom index`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom' }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); - }); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, namespaces, options) => { - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options) - ).rejects.toThrowError(createGenericNotFoundError(type, id)); - }; - const expectBadRequestError = async (type, id, namespaces, message) => { - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, namespaces) - ).rejects.toThrowError(createBadRequestError(message)); - }; - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id, [namespace1, namespace2]); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is not shareable`, async () => { - const test = async (type) => { - const message = `${type} doesn't support multiple namespaces`; - await expectBadRequestError(type, id, [namespace1, namespace2], message); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }; - await test('index-pattern'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); - }); - - it(`throws when namespaces is an empty array`, async () => { - const test = async (namespaces) => { - const message = 'namespaces must be a non-empty array of strings'; - await expectBadRequestError(type, id, namespaces, message); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }; - await test([]); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - await expectNotFoundError(type, id, [namespace1, namespace2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [namespace1, namespace2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id, [namespace1]); - await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during delete`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during delete`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - error: { type: 'index_not_found_exception' }, - }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES returns an unexpected response`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - result: 'something unexpected', - }) - ); - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1]) - ).rejects.toThrowError('Unexpected Elasticsearch DELETE response'); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id, [namespace1, namespace2]); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns an empty namespaces array on success (delete)`, async () => { - const test = async (namespaces) => { - const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces); - expect(result).toEqual({ namespaces: [] }); - client.delete.mockClear(); - }; - await test([namespace1]); - await test([namespace1, namespace2]); - }); - - it(`returns remaining namespaces on success (update)`, async () => { - const test = async (remaining) => { - const currentNamespaces = [namespace1].concat(remaining); - const result = await deleteFromNamespacesSuccess( - type, - id, - [namespace1], - currentNamespaces - ); - expect(result).toEqual({ namespaces: remaining }); - client.delete.mockClear(); - }; - await test([namespace2]); - await test([namespace2, namespace3]); - }); - - it(`succeeds when the document doesn't exist in all of the targeted namespaces`, async () => { - const namespaces = [namespace2]; - const currentNamespaces = [namespace1]; - const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces); - expect(result).toEqual({ namespaces: currentNamespaces }); - }); - }); - }); - describe('#update', () => { const id = 'logstash-*'; const type = 'index-pattern'; @@ -4722,4 +4203,65 @@ describe('SavedObjectsRepository', () => { ); }); }); + + describe('#collectMultiNamespaceReferences', () => { + afterEach(() => { + mockCollectMultiNamespaceReferences.mockReset(); + }); + + it('passes arguments to the collectMultiNamespaceReferences module and returns the result', async () => { + const objects = Symbol(); + const expectedResult = Symbol(); + mockCollectMultiNamespaceReferences.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.collectMultiNamespaceReferences(objects) + ).resolves.toEqual(expectedResult); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledWith( + expect.objectContaining({ objects }) + ); + }); + + it('returns an error from the collectMultiNamespaceReferences module', async () => { + const expectedResult = new Error('Oh no!'); + mockCollectMultiNamespaceReferences.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.collectMultiNamespaceReferences([])).rejects.toEqual( + expectedResult + ); + }); + }); + + describe('#updateObjectsSpaces', () => { + afterEach(() => { + mockUpdateObjectsSpaces.mockReset(); + }); + + it('passes arguments to the updateObjectsSpaces module and returns the result', async () => { + const objects = Symbol(); + const spacesToAdd = Symbol(); + const spacesToRemove = Symbol(); + const options = Symbol(); + const expectedResult = Symbol(); + mockUpdateObjectsSpaces.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).resolves.toEqual(expectedResult); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledWith( + expect.objectContaining({ objects, spacesToAdd, spacesToRemove, options }) + ); + }); + + it('returns an error from the updateObjectsSpaces module', async () => { + const expectedResult = new Error('Oh no!'); + mockUpdateObjectsSpaces.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.updateObjectsSpaces([], [], [])).rejects.toEqual( + expectedResult + ); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.test.mock.ts b/src/core/server/saved_objects/service/lib/repository.test.mock.ts index 3eba77b4658198e..f044fe9279fbfe4 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.mock.ts @@ -6,6 +6,36 @@ * Side Public License, v 1. */ +import type { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import type * as InternalUtils from './internal_utils'; +import type { updateObjectsSpaces } from './update_objects_spaces'; + +export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction< + typeof collectMultiNamespaceReferences +>; + +jest.mock('./collect_multi_namespace_references', () => ({ + collectMultiNamespaceReferences: mockCollectMultiNamespaceReferences, +})); + +export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getBulkOperationError'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + getBulkOperationError: mockGetBulkOperationError, + }; +}); + +export const mockUpdateObjectsSpaces = jest.fn() as jest.MockedFunction; + +jest.mock('./update_objects_spaces', () => ({ + updateObjectsSpaces: mockUpdateObjectsSpaces, +})); + export const pointInTimeFinderMock = jest.fn(); jest.doMock('./point_in_time_finder', () => ({ PointInTimeFinder: pointInTimeFinderMock, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2ef3be71407b0e9..c626a2b2acfb58b 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -48,10 +48,6 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateOptions, SavedObjectsDeleteOptions, - SavedObjectsAddToNamespacesOptions, - SavedObjectsAddToNamespacesResponse, - SavedObjectsDeleteFromNamespacesOptions, - SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, SavedObjectsResolveResponse, @@ -64,15 +60,31 @@ import { MutatingOperationRefreshSetting, } from '../../types'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; import { validateAndConvertAggregations } from './aggregations'; +import { + getBulkOperationError, + getExpectedVersionProperties, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from './internal_utils'; import { ALL_NAMESPACES_STRING, FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, SavedObjectsUtils, } from './utils'; +import { + collectMultiNamespaceReferences, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, +} from './collect_multi_namespace_references'; +import { + updateObjectsSpaces, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, +} from './update_objects_spaces'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -95,7 +107,7 @@ export interface SavedObjectsRepositoryOptions { index: string; mappings: IndexMapping; client: ElasticsearchClient; - typeRegistry: SavedObjectTypeRegistry; + typeRegistry: ISavedObjectTypeRegistry; serializer: SavedObjectsSerializer; migrator: IKibanaMigrator; allowedTypes: string[]; @@ -134,7 +146,7 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: boolean; } -const DEFAULT_REFRESH_SETTING = 'wait_for'; +export const DEFAULT_REFRESH_SETTING = 'wait_for'; /** * See {@link SavedObjectsRepository} @@ -160,7 +172,7 @@ export class SavedObjectsRepository { private _migrator: IKibanaMigrator; private _index: string; private _mappings: IndexMapping; - private _registry: SavedObjectTypeRegistry; + private _registry: ISavedObjectTypeRegistry; private _allowedTypes: string[]; private readonly client: RepositoryEsClient; private _serializer: SavedObjectsSerializer; @@ -176,7 +188,7 @@ export class SavedObjectsRepository { */ public static createRepository( migrator: IKibanaMigrator, - typeRegistry: SavedObjectTypeRegistry, + typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, @@ -511,16 +523,11 @@ export class SavedObjectsRepository { } const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const { error, ...rawResponse } = Object.values( - bulkResponse?.body.items[esRequestIndex] ?? {} - )[0] as any; + const rawResponse = Object.values(bulkResponse?.body.items[esRequestIndex] ?? {})[0] as any; + const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse); if (error) { - return { - id: requestedId, - type: rawMigratedDoc._source.type, - error: getBulkOperationError(error, rawMigratedDoc._source.type, requestedId), - }; + return { type: rawMigratedDoc._source.type, id: requestedId, error }; } // When method == 'index' the bulkResponse doesn't include the indexed @@ -989,7 +996,7 @@ export class SavedObjectsRepository { } // @ts-expect-error MultiGetHit._source is optional - return this.getSavedObjectFromSource(type, id, doc); + return getSavedObjectFromSource(this._registry, type, id, doc); }), }; } @@ -1033,7 +1040,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return this.getSavedObjectFromSource(type, id, body); + return getSavedObjectFromSource(this._registry, type, id, body); } /** @@ -1138,20 +1145,25 @@ export class SavedObjectsRepository { if (foundExactMatch && foundAliasMatch) { return { // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'conflict', aliasTargetId: legacyUrlAlias.targetId, }; } else if (foundExactMatch) { return { // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'exactMatch', }; } else if (foundAliasMatch) { return { - // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc), + saved_object: getSavedObjectFromSource( + this._registry, + type, + legacyUrlAlias.targetId, + // @ts-expect-error MultiGetHit._source is optional + aliasMatchDoc + ), outcome: 'aliasMatch', aliasTargetId: legacyUrlAlias.targetId, }; @@ -1263,169 +1275,52 @@ export class SavedObjectsRepository { } /** - * Adds one or more namespaces to a given multi-namespace saved object. This method and - * [`deleteFromNamespaces`]{@link SavedObjectsRepository.deleteFromNamespaces} are the only ways to change which Spaces a multi-namespace - * saved object is shared to. + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + * + * @param objects The objects to get the references for. */ - async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ): Promise { - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `${type} doesn't support multiple namespaces` - ); - } - - if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'namespaces must be a non-empty array of strings' - ); - } - - const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used - - const rawId = this._serializer.generateRawId(undefined, type, id); - const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); - // there should never be a case where a multi-namespace object does not have any existing namespaces - // however, it is a possibility if someone manually modifies the document in Elasticsearch - const time = this._getCurrentTime(); - - const doc = { - updated_at: time, - namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces, - }; - - const { statusCode } = await this.client.update( - { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult), - refresh, - body: { - doc, - }, - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - return { namespaces: doc.namespaces }; + async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ) { + return collectMultiNamespaceReferences({ + registry: this._registry, + allowedTypes: this._allowedTypes, + client: this.client, + serializer: this._serializer, + getIndexForType: this.getIndexForType.bind(this), + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + objects, + options, + }); } /** - * 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`]{@link SavedObjectsRepository.addToNamespaces} are the only ways to change which Spaces a - * multi-namespace saved object is shared to. + * Updates one or more objects to add and/or remove them from specified spaces. + * + * @param objects + * @param spacesToAdd + * @param spacesToRemove + * @param options */ - async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ): Promise { - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `${type} doesn't support multiple namespaces` - ); - } - - if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'namespaces must be a non-empty array of strings' - ); - } - - const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used - - const rawId = this._serializer.generateRawId(undefined, type, id); - const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); - // if there are somehow no existing namespaces, allow the operation to proceed and delete this saved object - const remainingNamespaces = existingNamespaces?.filter((x) => !namespaces.includes(x)); - - if (remainingNamespaces?.length) { - // if there is 1 or more namespace remaining, update the saved object - const time = this._getCurrentTime(); - - const doc = { - updated_at: time, - namespaces: remainingNamespaces, - }; - - const { statusCode } = await this.client.update( - { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - - body: { - doc, - }, - }, - { - ignore: [404], - } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return { namespaces: doc.namespaces }; - } else { - // if there are no namespaces remaining, delete the saved object - const { body, statusCode } = await this.client.delete( - { - id: this._serializer.generateRawId(undefined, type, id), - refresh, - ...getExpectedVersionProperties(undefined, preflightResult), - index: this.getIndexForType(type), - }, - { - ignore: [404], - } - ); - - const deleted = body.result === 'deleted'; - if (deleted) { - return { namespaces: [] }; - } - - const deleteDocNotFound = body.result === 'not_found'; - // @ts-expect-error - const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; - if (deleteDocNotFound || deleteIndexNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ - type, - id, - response: { body, statusCode }, - })}` - ); - } + async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return updateObjectsSpaces({ + registry: this._registry, + allowedTypes: this._allowedTypes, + client: this.client, + serializer: this._serializer, + getIndexForType: this.getIndexForType.bind(this), + objects, + spacesToAdd, + spacesToRemove, + options, + }); } /** @@ -1617,21 +1512,19 @@ export class SavedObjectsRepository { const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; const response = bulkUpdateResponse?.body.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { type, id, error }; + } + // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. - const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values( - response - )[0] as any; + const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse; // eslint-disable-next-line @typescript-eslint/naming-convention const { [type]: attributes, references, updated_at } = documentToSave; - if (error) { - return { - id, - type, - error: getBulkOperationError(error, type, id), - }; - } const { originId } = get._source; return { @@ -2055,10 +1948,10 @@ export class SavedObjectsRepository { * } * ``` */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies - ): ISavedObjectsPointInTimeFinder { + ): ISavedObjectsPointInTimeFinder { return new PointInTimeFinder(findOptions, { logger: this._logger, client: this, @@ -2108,28 +2001,8 @@ export class SavedObjectsRepository { return omit(savedObject, ['namespace']) as SavedObject; } - /** - * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as - * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the - * document's `namespaces` value includes the string representation of the given namespace. - * - * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID - * format mentioned above do not apply. - */ - private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace?: string) { - const rawDocType = raw._source.type; - - // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees - // of the document ID format and don't need to check this - if (!this._registry.isMultiNamespace(rawDocType)) { - return true; - } - - const namespaces = raw._source.namespaces; - const existsInNamespace = - namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) || - namespaces?.includes('*'); - return existsInNamespace ?? false; + private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) { + return rawDocExistsInNamespace(this._registry, raw, namespace); } /** @@ -2204,34 +2077,6 @@ export class SavedObjectsRepository { return body; } - private getSavedObjectFromSource( - type: string, - id: string, - doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } - ): SavedObject { - const { originId, updated_at: updatedAt } = doc._source; - - let namespaces: string[] = []; - if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(doc._source.namespace), - ]; - } - - return { - id, - type, - namespaces, - ...(originId && { originId }), - ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - coreMigrationVersion: doc._source.coreMigrationVersion, - }; - } - private async resolveExactMatch( type: string, id: string, @@ -2242,43 +2087,6 @@ export class SavedObjectsRepository { } } -function getBulkOperationError( - error: { type: string; reason?: string; index?: string }, - type: string, - id: string -) { - switch (error.type) { - case 'version_conflict_engine_exception': - return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); - case 'document_missing_exception': - return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); - case 'index_not_found_exception': - return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); - default: - return { - message: error.reason || JSON.stringify(error), - }; - } -} - -/** - * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. - * - * @param version Optional version specified by the consumer. - * @param document Optional existing document that was obtained in a preflight operation. - */ -function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { - if (version) { - return decodeRequestVersion(version); - } else if (document) { - return { - if_seq_no: document._seq_no, - if_primary_term: document._primary_term, - }; - } - return {}; -} - /** * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts new file mode 100644 index 000000000000000..d7aa762e01aab7e --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as InternalUtils from './internal_utils'; + +export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getBulkOperationError'] +>; +export const mockGetExpectedVersionProperties = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getExpectedVersionProperties'] +>; +export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< + typeof InternalUtils['rawDocExistsInNamespace'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + getBulkOperationError: mockGetBulkOperationError, + getExpectedVersionProperties: mockGetExpectedVersionProperties, + rawDocExistsInNamespace: mockRawDocExistsInNamespace, + }; +}); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts new file mode 100644 index 000000000000000..489432a4ab1692f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts @@ -0,0 +1,453 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + mockGetBulkOperationError, + mockGetExpectedVersionProperties, + mockRawDocExistsInNamespace, +} from './update_objects_spaces.test.mock'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { SavedObjectsSerializer } from '../../serialization'; +import type { + SavedObjectsUpdateObjectsSpacesObject, + UpdateObjectsSpacesParams, +} from './update_objects_spaces'; +import { updateObjectsSpaces } from './update_objects_spaces'; + +type SetupParams = Partial< + Pick +>; + +const EXISTING_SPACE = 'existing-space'; +const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; +const EXPECTED_VERSION_PROPS = { if_seq_no: 1, if_primary_term: 1 }; +const BULK_ERROR = { + error: 'Oh no, a bulk error!', + type: 'error_type', + message: 'error_message', + statusCode: 400, +}; + +const SHAREABLE_OBJ_TYPE = 'type-a'; +const NON_SHAREABLE_OBJ_TYPE = 'type-b'; +const SHAREABLE_HIDDEN_OBJ_TYPE = 'type-c'; + +const mockCurrentTime = new Date('2021-05-01T10:20:30Z'); + +beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(mockCurrentTime); +}); + +beforeEach(() => { + mockGetExpectedVersionProperties.mockReturnValue(EXPECTED_VERSION_PROPS); + mockRawDocExistsInNamespace.mockReset(); + mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +describe('#updateObjectsSpaces', () => { + let client: DeeplyMockedKeys; + + /** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `updateObjectsSpaces` */ + function setup({ objects = [], spacesToAdd = [], spacesToRemove = [], options }: SetupParams) { + const registry = typeRegistryMock.create(); + registry.isShareable.mockImplementation( + (type) => [SHAREABLE_OBJ_TYPE, SHAREABLE_HIDDEN_OBJ_TYPE].includes(type) // NON_SHAREABLE_OBJ_TYPE is excluded + ); + client = elasticsearchClientMock.createElasticsearchClient(); + const serializer = new SavedObjectsSerializer(registry); + return { + registry, + allowedTypes: [SHAREABLE_OBJ_TYPE, NON_SHAREABLE_OBJ_TYPE], // SHAREABLE_HIDDEN_OBJ_TYPE is excluded + client, + serializer, + getIndexForType: (type: string) => `index-for-${type}`, + objects, + spacesToAdd, + spacesToRemove, + options, + }; + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockMgetResults(...results: Array<{ found: boolean }>) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + docs: results.map((x) => + x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { namespaces: [EXISTING_SPACE] }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + } + ), + }) + ); + } + + /** Asserts that mget is called for the given objects */ + function expectMgetArgs(...objects: SavedObjectsUpdateObjectsSpacesObject[]) { + const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); + expect(client.mget).toHaveBeenCalledWith({ body: { docs } }, expect.anything()); + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockBulkResults(...results: Array<{ error: boolean }>) { + results.forEach(({ error }) => { + if (error) { + mockGetBulkOperationError.mockReturnValueOnce(BULK_ERROR); + } else { + mockGetBulkOperationError.mockReturnValueOnce(undefined); + } + }); + client.bulk.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + items: results.map(() => ({})), // as long as the result does not contain an error field, it is treated as a success + errors: false, + took: 0, + }) + ); + } + + /** Asserts that mget is called for the given objects */ + function expectBulkArgs( + ...objectActions: Array<{ + object: { type: string; id: string; namespaces?: string[] }; + action: 'update' | 'delete'; + }> + ) { + const body = objectActions.flatMap( + ({ object: { type, id, namespaces = expect.any(Array) }, action }) => { + const operation = { + [action]: { + _id: `${type}:${id}`, + _index: `index-for-${type}`, + ...EXPECTED_VERSION_PROPS, + }, + }; + return action === 'update' + ? [operation, { doc: { namespaces, updated_at: mockCurrentTime.toISOString() } }] // 'update' uses an operation and document metadata + : [operation]; // 'delete' only uses an operation + } + ); + expect(client.bulk).toHaveBeenCalledWith(expect.objectContaining({ body })); + } + + beforeEach(() => { + mockGetBulkOperationError.mockReset(); // reset calls and return undefined by default + }); + + describe('errors', () => { + it('throws when spacesToAdd and spacesToRemove are empty', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const params = setup({ objects }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow( + 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings: Bad Request' + ); + }); + + it('throws when spacesToAdd and spacesToRemove intersect', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space', 'bar-space']; + const spacesToRemove = ['bar-space', 'baz-space']; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow( + 'spacesToAdd and spacesToRemove cannot contain any of the same strings: Bad Request' + ); + }); + + it('throws when mget cluster call fails', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('mget error')) + ); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow('mget error'); + }); + + it('throws when bulk cluster call fails', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }); + client.bulk.mockReturnValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('bulk error')) + ); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow('bulk error'); + }); + + it('returns mix of type errors, mget/bulk cluster errors, and successes', async () => { + const obj1 = { type: SHAREABLE_HIDDEN_OBJ_TYPE, id: 'id-1' }; // invalid type (Not Found) + const obj2 = { type: NON_SHAREABLE_OBJ_TYPE, id: 'id-2' }; // non-shareable type (Bad Request) + // obj3 below is mocking an example where a SOC wrapper attempted to retrieve it in a pre-flight request but it was not found. + // Since it has 'spaces: []', that indicates it should be skipped for cluster calls and just returned as a Not Found error. + // Realistically this would not be intermingled with other requested objects that do not have 'spaces' arrays, but it's fine for this + // specific test case. + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [] }; // does not exist (Not Found) + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4' }; // mget error (found but doesn't exist in the current space) + const obj5 = { type: SHAREABLE_OBJ_TYPE, id: 'id-5' }; // mget error (Not Found) + const obj6 = { type: SHAREABLE_OBJ_TYPE, id: 'id-6' }; // bulk error (mocked as BULK_ERROR) + const obj7 = { type: SHAREABLE_OBJ_TYPE, id: 'id-7' }; // success + + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }, { found: false }, { found: true }, { found: true }); // results for obj4, obj5, obj6, and obj7 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj6 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj7 + mockBulkResults({ error: true }, { error: false }); // results for obj6 and obj7 + + const result = await updateObjectsSpaces(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(obj4, obj5, obj6, obj7); + expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ action: 'update', object: obj6 }, { action: 'update', object: obj7 }); + expect(result.objects).toEqual([ + { ...obj1, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj2, spaces: [], error: expect.objectContaining({ error: 'Bad Request' }) }, + { ...obj3, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj4, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj5, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj6, spaces: [], error: BULK_ERROR }, + { ...obj7, spaces: [EXISTING_SPACE, 'foo-space'] }, + ]); + }); + }); + + // Note: these test cases do not include requested objects that will result in errors (those are covered above) + describe('cluster and module calls', () => { + it('mget call skips objects that have "spaces" defined', async () => { + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2' }; // will be passed to mget + + const objects = [obj1, obj2]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }); // result for obj2 + mockBulkResults({ error: false }, { error: false }); // results for obj1 and obj2 + + await updateObjectsSpaces(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(obj2); + }); + + it('does not call mget if all objects have "spaces" defined', async () => { + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved + + const objects = [obj1]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockBulkResults({ error: false }); // result for obj1 + + await updateObjectsSpaces(params); + expect(client.mget).not.toHaveBeenCalled(); + }); + + describe('bulk call skips objects that will not be changed', () => { + it('when adding spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ + action: 'update', + object: { ...obj2, namespaces: [space2, space1] }, + }); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left) + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space2] } }, + { action: 'delete', object: obj3 } + ); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2 + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2 + + const objects = [obj1, obj2, obj3, obj4]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space3, space1] } }, + { action: 'update', object: { ...obj3, namespaces: [space1] } }, + { action: 'update', object: { ...obj4, namespaces: [space3, space1] } } + ); + }); + }); + + describe('does not call bulk if all objects do not need to be changed', () => { + it('when adding spaces', async () => { + const space = 'space-to-add'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space] }; // will not be changed + + const objects = [obj1]; + const spacesToAdd = [space]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + + const objects = [obj1]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + + const objects = [obj1]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + }); + }); + + describe('returns expected results', () => { + it('when adding spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space1] }, + { ...obj2, spaces: [space2, space1] }, + ]); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left) + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space2] }, + { ...obj2, spaces: [space2] }, + { ...obj3, spaces: [] }, + ]); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2 + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2 + + const objects = [obj1, obj2, obj3, obj4]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space1] }, + { ...obj2, spaces: [space3, space1] }, + { ...obj3, spaces: [space1] }, + { ...obj4, spaces: [space3, space1] }, + ]); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts new file mode 100644 index 000000000000000..079549265385cdc --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { BulkOperationContainer, MultiGetOperation } from '@elastic/elasticsearch/api/types'; +import intersection from 'lodash/intersection'; + +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsRawDocSource, SavedObjectsSerializer } from '../../serialization'; +import type { + MutatingOperationRefreshSetting, + SavedObjectError, + SavedObjectsBaseOptions, +} from '../../types'; +import type { DecoratedError } from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; +import { + getBulkOperationError, + getExpectedVersionProperties, + rawDocExistsInNamespace, +} from './internal_utils'; +import { DEFAULT_REFRESH_SETTING } from './repository'; +import type { RepositoryEsClient } from './repository_es_client'; + +/** + * An object that should have its spaces updated. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesObject { + /** The type of the object to update */ + id: string; + /** The ID of the object to update */ + type: string; + /** + * The space(s) that the object to update currently exists in. This is only intended to be used by SOC wrappers. + * + * @internal + */ + spaces?: string[]; + /** + * The version of the object to update; this is used for optimistic concurrency control. This is only intended to be used by SOC wrappers. + * + * @internal + */ + version?: string; +} + +/** + * Options for the update operation. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * The response when objects' spaces are updated. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesResponse { + objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +} + +/** + * Details about a specific object's update result. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesResponseObject { + /** The type of the referenced object */ + type: string; + /** The ID of the referenced object */ + id: string; + /** The space(s) that the referenced object exists in */ + spaces: string[]; + /** Included if there was an error updating this object's spaces */ + error?: SavedObjectError; +} + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Left = { tag: 'Left'; error: SavedObjectsUpdateObjectsSpacesResponseObject }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Right = { tag: 'Right'; value: Record }; +type Either = Left | Right; +const isLeft = (either: Either): either is Left => either.tag === 'Left'; +const isRight = (either: Either): either is Right => either.tag === 'Right'; + +/** + * Parameters for the updateObjectsSpaces function. + * + * @internal + */ +export interface UpdateObjectsSpacesParams { + registry: ISavedObjectTypeRegistry; + allowedTypes: string[]; + client: RepositoryEsClient; + serializer: SavedObjectsSerializer; + getIndexForType: (type: string) => string; + objects: SavedObjectsUpdateObjectsSpacesObject[]; + spacesToAdd: string[]; + spacesToRemove: string[]; + options?: SavedObjectsUpdateObjectsSpacesOptions; +} + +/** + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + */ +export async function updateObjectsSpaces({ + registry, + allowedTypes, + client, + serializer, + getIndexForType, + objects, + spacesToAdd, + spacesToRemove, + options = {}, +}: UpdateObjectsSpacesParams): Promise { + if (!spacesToAdd.length && !spacesToRemove.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings' + ); + } + if (intersection(spacesToAdd, spacesToRemove).length > 0) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'spacesToAdd and spacesToRemove cannot contain any of the same strings' + ); + } + + const { namespace } = options; + + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map((object) => { + const { type, id, spaces, version } = object; + + if (!allowedTypes.includes(type)) { + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + if (!registry.isShareable(type)) { + const error = errorContent( + SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ) + ); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + + return { + tag: 'Right' as 'Right', + value: { + type, + id, + spaces, + version, + ...(!spaces && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); + + const bulkGetDocs = expectedBulkGetResults.reduce((acc, x) => { + if (isRight(x) && x.value.esRequestIndex !== undefined) { + acc.push({ + _id: serializer.generateRawId(undefined, x.value.type, x.value.id), + _index: getIndexForType(x.value.type), + _source: ['type', 'namespaces'], + }); + } + return acc; + }, []); + const bulkGetResponse = bulkGetDocs.length + ? await client.mget( + { body: { docs: bulkGetDocs } }, + { ignore: [404] } + ) + : undefined; + + const time = new Date().toISOString(); + let bulkOperationRequestIndexCounter = 0; + const bulkOperationParams: BulkOperationContainer[] = []; + const expectedBulkOperationResults: Either[] = expectedBulkGetResults.map( + (expectedBulkGetResult) => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + const { id, type, spaces, version, esRequestIndex } = expectedBulkGetResult.value; + + let currentSpaces: string[] = spaces; + let versionProperties; + if (esRequestIndex !== undefined) { + const doc = bulkGetResponse?.body.docs[esRequestIndex]; + // @ts-expect-error MultiGetHit._source is optional + if (!doc?.found || !rawDocExistsInNamespace(registry, doc, namespace)) { + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + currentSpaces = doc._source?.namespaces ?? []; + // @ts-expect-error MultiGetHit._source is optional + versionProperties = getExpectedVersionProperties(version, doc); + } else if (spaces?.length === 0) { + // A SOC wrapper attempted to retrieve this object in a pre-flight request and it was not found. + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } else { + versionProperties = getExpectedVersionProperties(version); + } + + const { newSpaces, isUpdateRequired } = getNewSpacesArray( + currentSpaces, + spacesToAdd, + spacesToRemove + ); + const expectedResult = { + type, + id, + newSpaces, + ...(isUpdateRequired && { esRequestIndex: bulkOperationRequestIndexCounter++ }), + }; + + if (isUpdateRequired) { + const documentMetadata = { + _id: serializer.generateRawId(undefined, type, id), + _index: getIndexForType(type), + ...versionProperties, + }; + if (newSpaces.length) { + const documentToSave = { updated_at: time, namespaces: newSpaces }; + // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional + bulkOperationParams.push({ update: documentMetadata }, { doc: documentToSave }); + } else { + // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional + bulkOperationParams.push({ delete: documentMetadata }); + } + } + + return { tag: 'Right' as 'Right', value: expectedResult }; + } + ); + + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const bulkOperationResponse = bulkOperationParams.length + ? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true }) + : undefined; + + return { + objects: expectedBulkOperationResults.map( + (expectedResult) => { + if (isLeft(expectedResult)) { + return expectedResult.error; + } + + const { type, id, newSpaces, esRequestIndex } = expectedResult.value; + if (esRequestIndex !== undefined) { + const response = bulkOperationResponse?.body.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { id, type, spaces: [], error }; + } + } + + return { id, type, spaces: newSpaces }; + } + ), + }; +} + +/** Extracts the contents of a decorated error to return the attributes for bulk operations. */ +function errorContent(error: DecoratedError) { + return error.output.payload; +} + +/** Gets the remaining spaces for an object after adding new ones and removing old ones. */ +function getNewSpacesArray( + existingSpaces: string[], + spacesToAdd: string[], + spacesToRemove: string[] +) { + const addSet = new Set(spacesToAdd); + const removeSet = new Set(spacesToRemove); + const newSpaces = existingSpaces + .filter((x) => { + addSet.delete(x); + return !removeSet.delete(x); + }) + .concat(Array.from(addSet)); + + const isAnySpaceAdded = addSet.size > 0; + const isAnySpaceRemoved = removeSet.size < spacesToRemove.length; + const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved; + + return { newSpaces, isUpdateRequired }; +} diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 544e92e32f1a172..e02387d41addf80 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -26,9 +26,9 @@ const create = () => { openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), removeReferencesTo: jest.fn(), + collectMultiNamespaceReferences: jest.fn(), + updateObjectsSpaces: jest.fn(), } as unknown) as jest.Mocked; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 29381c7e418b5fc..1a369475f2c6d70 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -237,52 +237,39 @@ test(`#bulkUpdate`, async () => { expect(result).toBe(returnValue); }); -test(`#addToNamespaces`, async () => { +test(`#collectMultiNamespaceReferences`, async () => { const returnValue = Symbol(); const mockRepository = { - addToNamespaces: jest.fn().mockResolvedValue(returnValue), + collectMultiNamespaceReferences: jest.fn().mockResolvedValue(returnValue), }; const client = new SavedObjectsClient(mockRepository); - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Symbol(); - const result = await client.addToNamespaces(type, id, namespaces, options); - - expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); - expect(result).toBe(returnValue); -}); - -test(`#deleteFromNamespaces`, async () => { - const returnValue = Symbol(); - const mockRepository = { - deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); + const objects = Symbol(); const options = Symbol(); - const result = await client.deleteFromNamespaces(type, id, namespaces, options); + const result = await client.collectMultiNamespaceReferences(objects, options); - expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(mockRepository.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); expect(result).toBe(returnValue); }); -test(`#removeReferencesTo`, async () => { +test(`#updateObjectsSpaces`, async () => { const returnValue = Symbol(); const mockRepository = { - removeReferencesTo: jest.fn().mockResolvedValue(returnValue), + updateObjectsSpaces: jest.fn().mockResolvedValue(returnValue), }; const client = new SavedObjectsClient(mockRepository); - const type = Symbol(); - const id = Symbol(); + const objects = Symbol(); + const spacesToAdd = Symbol(); + const spacesToRemove = Symbol(); const options = Symbol(); - const result = await client.removeReferencesTo(type, id, options); - - expect(mockRepository.removeReferencesTo).toHaveBeenCalledWith(type, id, options); + const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockRepository.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + options + ); expect(result).toBe(returnValue); }); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index bf5cae0736cad6e..af682cfb81296e0 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -11,6 +11,11 @@ import type { ISavedObjectsPointInTimeFinder, SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, } from './lib'; import { SavedObject, @@ -218,44 +223,6 @@ export interface SavedObjectsUpdateOptions extends SavedOb upsert?: Attributes; } -/** - * - * @public - */ -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ - version?: string; - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - -/** - * - * @public - */ -export interface SavedObjectsAddToNamespacesResponse { - /** The namespaces the object exists in after this operation is complete. */ - namespaces: string[]; -} - -/** - * - * @public - */ -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - -/** - * - * @public - */ -export interface SavedObjectsDeleteFromNamespacesResponse { - /** The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. */ - namespaces: string[]; -} - /** * * @public @@ -536,40 +503,6 @@ export class SavedObjectsClient { return await this._repository.update(type, id, attributes, options); } - /** - * Adds namespaces to a SavedObject - * - * @param type - * @param id - * @param namespaces - * @param options - */ - async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ): Promise { - return await this._repository.addToNamespaces(type, id, namespaces, options); - } - - /** - * Removes namespaces from a SavedObject - * - * @param type - * @param id - * @param namespaces - * @param options - */ - async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ): Promise { - return await this._repository.deleteFromNamespaces(type, id, namespaces, options); - } - /** * Bulk Updates multiple SavedObject at once * @@ -665,14 +598,49 @@ export class SavedObjectsClient { * } * ``` */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies - ): ISavedObjectsPointInTimeFinder { + ): ISavedObjectsPointInTimeFinder { return this._repository.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that SO client wrappers have their settings applied. ...dependencies, }); } + + /** + * Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. + * + * @param objects + * @param options + */ + async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ): Promise { + return await this._repository.collectMultiNamespaceReferences(objects, options); + } + + /** + * Updates one or more objects to add and/or remove them from specified spaces. + * + * @param objects + * @param spacesToAdd + * @param spacesToRemove + * @param options + */ + async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return await this._repository.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + } } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4c12ca53b90987e..3e6a69d159192f1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -976,6 +976,8 @@ export interface ElasticsearchStatusMeta { // (undocumented) incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; // (undocumented) + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; + // (undocumented) warningNodes: NodesVersionCompatibility['warningNodes']; } @@ -1253,9 +1255,9 @@ export type ISavedObjectsExporter = PublicMethodsOf; export type ISavedObjectsImporter = PublicMethodsOf; // @public (undocumented) -export interface ISavedObjectsPointInTimeFinder { +export interface ISavedObjectsPointInTimeFinder { close: () => Promise; - find: () => AsyncGenerator; + find: () => AsyncGenerator>; } // @public @@ -1727,6 +1729,8 @@ export interface NodesVersionCompatibility { // (undocumented) message?: string; // (undocumented) + nodesInfoRequestError?: Error; + // (undocumented) warningNodes: NodeInfo[]; } @@ -2140,6 +2144,7 @@ export type SavedObjectAttributeSingle = string | number | boolean | null | unde // @public (undocumented) export interface SavedObjectExportBaseOptions { excludeExportDetails?: boolean; + includeNamespaces?: boolean; includeReferencesDeep?: boolean; namespace?: string; request: KibanaRequest; @@ -2147,9 +2152,9 @@ export interface SavedObjectExportBaseOptions { // @public export interface SavedObjectMigrationContext { - convertToMultiNamespaceTypeVersion?: string; - log: SavedObjectsMigrationLogger; - migrationVersion: string; + readonly convertToMultiNamespaceTypeVersion?: string; + readonly log: SavedObjectsMigrationLogger; + readonly migrationVersion: string; } // @public @@ -2171,15 +2176,18 @@ export interface SavedObjectReference { type: string; } -// @public (undocumented) -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; - version?: string; -} - -// @public (undocumented) -export interface SavedObjectsAddToNamespacesResponse { - namespaces: string[]; +// @public +export interface SavedObjectReferenceWithContext { + id: string; + inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; + isMissing?: boolean; + spaces: string[]; + spacesWithMatchingAliases?: string[]; + type: string; } // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts @@ -2273,16 +2281,15 @@ export interface SavedObjectsCheckConflictsResponse { export class SavedObjectsClient { // @internal constructor(repository: ISavedObjectsRepository); - addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; - deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) @@ -2293,6 +2300,7 @@ export class SavedObjectsClient { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2337,6 +2345,25 @@ export interface SavedObjectsClosePointInTimeResponse { succeeded: boolean; } +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesObject { + // (undocumented) + id: string; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions { + purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +} + +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + // (undocumented) + objects: SavedObjectReferenceWithContext[]; +} + // @public export interface SavedObjectsComplexFieldMapping { // (undocumented) @@ -2397,16 +2424,6 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: boolean; } -// @public (undocumented) -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; -} - -// @public (undocumented) -export interface SavedObjectsDeleteFromNamespacesResponse { - namespaces: string[]; -} - // @public (undocumented) export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { force?: boolean; @@ -2880,21 +2897,20 @@ export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBase // @public (undocumented) export class SavedObjectsRepository { - addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: IKibanaMigrator, typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; - deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; @@ -2903,6 +2919,7 @@ export class SavedObjectsRepository { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2934,7 +2951,7 @@ export class SavedObjectsSerializer { generateRawId(namespace: string | undefined, type: string, id: string): string; generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string; isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean; - rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; + rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; } @@ -3000,6 +3017,35 @@ export interface SavedObjectsTypeMappingDefinition { properties: SavedObjectsMappingProperties; } +// @public +export interface SavedObjectsUpdateObjectsSpacesObject { + id: string; + // @internal + spaces?: string[]; + type: string; + // @internal + version?: string; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesResponse { + // (undocumented) + objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesResponseObject { + error?: SavedObjectError; + id: string; + spaces: string[]; + type: string; +} + // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; diff --git a/src/core/server/types.ts b/src/core/server/types.ts index be07a3cfb1fd32d..77b5378f9477fa6 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -37,6 +37,10 @@ export type { SavedObjectsClientContract, SavedObjectsNamespaceType, } from './saved_objects/types'; +export type { + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from './saved_objects/service'; export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types'; export * from './ui_settings/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index 0f13c82eed11a8d..5029f246367d354 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { SavedObjectsClientContract } from '../saved_objects/types'; import { UiSettingsParams, UserProvidedValues, PublicUiSettingsParams } from '../../types'; export type { 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 7f2147ea76beb0f..b73d25b7cf6e05f 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -558,6 +558,15 @@ describe('ui settings', () => { bar: 'user-provided', }); }); + + it('throws if mutates the result of getAll()', async () => { + const { uiSettings } = setup({ esDocSource: {} }); + const result = await uiSettings.getAll(); + + expect(() => { + result.foo = 'bar'; + }).toThrow(); + }); }); describe('#get()', () => { diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 3df48649781e9b0..ee0dcbea2a9aa54 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { defaultsDeep, omit } from 'lodash'; +import { omit } from 'lodash'; import { SavedObjectsErrorHelpers } from '../saved_objects'; import { SavedObjectsClientContract } from '../saved_objects/types'; @@ -35,10 +35,7 @@ interface UserProvidedValue { isOverridden?: boolean; } -type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; - type UserProvided = Record>; -type UiSettingsRaw = Record; export class UiSettingsClient implements IUiSettingsClient { private readonly type: UiSettingsServiceOptions['type']; @@ -47,6 +44,7 @@ export class UiSettingsClient implements IUiSettingsClient { private readonly savedObjectsClient: UiSettingsServiceOptions['savedObjectsClient']; private readonly overrides: NonNullable; private readonly defaults: NonNullable; + private readonly defaultValues: Record; private readonly log: Logger; private readonly cache: Cache; @@ -56,10 +54,15 @@ export class UiSettingsClient implements IUiSettingsClient { this.id = id; this.buildNum = buildNum; this.savedObjectsClient = savedObjectsClient; - this.defaults = defaults; this.overrides = overrides; this.log = log; this.cache = new Cache(); + this.defaults = defaults; + const defaultValues: Record = {}; + Object.keys(this.defaults).forEach((key) => { + defaultValues[key] = this.defaults[key].value; + }); + this.defaultValues = defaultValues; } getRegistered() { @@ -72,17 +75,21 @@ export class UiSettingsClient implements IUiSettingsClient { async get(key: string): Promise { const all = await this.getAll(); - return all[key]; + return all[key] as T; } async getAll() { - const raw = await this.getRaw(); + const result = { ...this.defaultValues }; - return Object.keys(raw).reduce((all, key) => { - const item = raw[key]; - all[key] = ('userValue' in item ? item.userValue : item.value) as T; - return all; - }, {} as Record); + const userProvided = await this.getUserProvided(); + Object.keys(userProvided).forEach((key) => { + if (userProvided[key].userValue !== undefined) { + result[key] = userProvided[key].userValue; + } + }); + + Object.freeze(result); + return result as Record; } async getUserProvided(): Promise> { @@ -142,11 +149,6 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private async getRaw(): Promise { - const userProvided = await this.getUserProvided(); - return defaultsDeep({}, userProvided, this.defaults); - } - private validateKey(key: string, value: unknown) { const definition = this.defaults[key]; if (value === null || definition === undefined) return; diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index d97dc8e70cd9b16..4ce71b24332c1dc 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -19,11 +19,7 @@ interface Options { type CircularDepList = Set; -const allowedList: CircularDepList = new Set([ - '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', -]); +const allowedList: CircularDepList = new Set([]); run( async ({ flags, log }) => { diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7852c9da7898fb7..bd72af3c0e8cf7c 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -146,7 +146,7 @@ export function populateContext(tokenPath, context, editor, includeAutoComplete, if (!wsToUse && walkStates.length > 1 && !includeAutoComplete) { console.info( - "more then one context active for current path, but autocomplete isn't requested", + "more than one context active for current path, but autocomplete isn't requested", walkStates ); } diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts index 90839a18e1210d8..4e42e3c21d2ad88 100644 --- a/src/plugins/console/server/config.ts +++ b/src/plugins/console/server/config.ts @@ -15,8 +15,6 @@ export const config = schema.object( enabled: schema.boolean({ defaultValue: true }), proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), - - // This does not actually work, track this issue: https://github.com/elastic/kibana/issues/55576 proxyConfig: schema.arrayOf( schema.object({ match: schema.object({ diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts index 017b7d804c8729c..1ed5bfba3abb911 100644 --- a/src/plugins/dashboard/common/index.ts +++ b/src/plugins/dashboard/common/index.ts @@ -24,3 +24,7 @@ export { } from './types'; export { migratePanelsTo730 } from './migrate_to_730_panels'; + +export const UI_SETTINGS = { + ENABLE_LABS_UI: 'labs:dashboard:enable_ui', +}; diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index 16ab470ce7d6f7a..9757415a7bc3604 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import semverSatisfies from 'semver/functions/satisfies'; +import Semver from 'semver'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; @@ -24,7 +23,7 @@ export interface SavedObjectAttributesAndReferences { } const isPre730Panel = (panel: Record): boolean => { - return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true; + return 'version' in panel ? Semver.gt('7.3.0', panel.version) : true; }; function dashboardAttributesToState( @@ -82,6 +81,9 @@ export function extractReferences( } const { panels, state } = dashboardAttributesToState(attributes); + if (!Array.isArray(panels)) { + return { attributes, references }; + } if (((panels as unknown) as Array>).some(isPre730Panel)) { return pre730ExtractReferences({ attributes, references }, deps); diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 1156bf8c6e0d148..0f7acfbb3f5f667 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -14,6 +14,7 @@ import { CoreStart } from 'kibana/public'; import { coreMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; +import { getStubPluginServices } from '../../../../presentation_util/public'; import { EmbeddableInput, @@ -63,6 +64,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index 5d3527431a048c3..08e115ffca90877 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -21,6 +21,7 @@ import { CONTACT_CARD_EMBEDDABLE, } from '../../services/embeddable_test_samples'; import { ErrorEmbeddable, IContainer, isErrorEmbeddable } from '../../services/embeddable'; +import { getStubPluginServices } from '../../../../presentation_util/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -53,6 +54,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx index 280e56a230dacfc..48bb787116862b6 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx @@ -12,6 +12,8 @@ import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helper import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; import { isErrorEmbeddable } from '../../services/embeddable'; +import { getStubPluginServices } from '../../../../presentation_util/public'; + import { CONTACT_CARD_EMBEDDABLE, ContactCardEmbeddableFactory, @@ -45,6 +47,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + presentationUtil: getStubPluginServices(), }; const input = getSampleDashboardInput({ panels: { 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 index d82e8cbe9d2e5af..20144b47e474bdc 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -24,6 +24,7 @@ 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'; +import { getStubPluginServices } from '../../../../presentation_util/public'; describe('Export CSV action', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); @@ -59,6 +60,7 @@ describe('Export CSV action', () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx index 5958563c35b1531..3d001913f4c75f6 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -27,6 +27,7 @@ import { ContactCardEmbeddableOutput, CONTACT_CARD_EMBEDDABLE, } from '../../services/embeddable_test_samples'; +import { getStubPluginServices } from '../../../../presentation_util/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -60,6 +61,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx index 00de58f8954e030..3204a0b38fc845f 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx @@ -27,6 +27,7 @@ import { ContactCardEmbeddableOutput, ContactCardEmbeddable, } from '../../services/embeddable_test_samples'; +import { getStubPluginServices } from '../../../../presentation_util/public'; describe('LibraryNotificationPopover', () => { const { setup, doStart } = embeddablePluginMock.createInstance(); @@ -55,6 +56,7 @@ describe('LibraryNotificationPopover', () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx index 600fca28eb815c3..c8fe39f63fa23b7 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx @@ -21,6 +21,7 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../../services/embeddable_test_samples'; +import { getStubPluginServices } from '../../../../presentation_util/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -46,6 +47,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; const input = getSampleDashboardInput({ panels: { diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index a119f3364df4f48..daa21b034f7c28b 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -29,6 +29,7 @@ import { ContactCardEmbeddableOutput, CONTACT_CARD_EMBEDDABLE, } from '../../services/embeddable_test_samples'; +import { getStubPluginServices } from '../../../../presentation_util/public'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory( @@ -55,6 +56,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + presentationUtil: getStubPluginServices(), }; container = new DashboardContainer(getSampleDashboardInput(), containerOptions); diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index ed68afc5e97b153..d5eddf6bb486479 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -30,6 +30,7 @@ import { import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils'; import { KibanaContextProvider } from '../services/kibana_react'; + import { AppMountParameters, CoreSetup, @@ -81,10 +82,12 @@ export async function mountApp({ kibanaLegacy: { dashboardConfig }, savedObjectsTaggingOss, visualizations, + presentationUtil, } = pluginsStart; const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined; - const activeSpaceId = spacesApi && (await spacesApi.activeSpace$.pipe(first()).toPromise())?.id; + const activeSpaceId = + spacesApi && (await spacesApi.getActiveSpace$().pipe(first()).toPromise())?.id; let globalEmbedSettings: DashboardEmbedSettings | undefined; const dashboardServices: DashboardAppServices = { @@ -207,22 +210,24 @@ export async function mountApp({ const app = ( - - - - - - - - - - + + + + + + + + + + + + ); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx index 19c541303fd030a..41054b377d22e29 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx @@ -38,6 +38,9 @@ import { } from '../../../../../core/public/mocks'; import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { uiActionsPluginMock } from '../../../../ui_actions/public/mocks'; +import { getStubPluginServices } from '../../../../presentation_util/public'; + +const presentationUtil = getStubPluginServices(); const options: DashboardContainerServices = { application: {} as any, @@ -50,6 +53,7 @@ const options: DashboardContainerServices = { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + presentationUtil, }; beforeEach(() => { @@ -233,17 +237,19 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { const component = mount( - Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => null) as any} - notifications={{} as any} - application={options.application} - overlays={{} as any} - inspector={inspector} - SavedObjectFinder={() => null} - /> + + Promise.resolve([])} + getAllEmbeddableFactories={(() => []) as any} + getEmbeddableFactory={(() => null) as any} + notifications={{} as any} + application={options.application} + overlays={{} as any} + inspector={inspector} + SavedObjectFinder={() => null} + /> + ); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 9013a832d27a4f7..92b0727d2458cf5 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -38,6 +38,7 @@ import { import { PLACEHOLDER_EMBEDDABLE } from './placeholder'; import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement'; import { DashboardCapabilities } from '../types'; +import { PresentationUtilPluginStart } from '../../services/presentation_util'; export interface DashboardContainerInput extends ContainerInput { dashboardCapabilities?: DashboardCapabilities; @@ -68,6 +69,7 @@ export interface DashboardContainerServices { embeddable: EmbeddableStart; uiActions: UiActionsStart; http: CoreStart['http']; + presentationUtil: PresentationUtilPluginStart; } interface IndexSignature { @@ -245,7 +247,9 @@ export class DashboardContainer extends Container - + + + , dom diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx index 9c82d03396a4830..991033b9a0d6a5f 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx @@ -22,8 +22,10 @@ import { ContactCardEmbeddableFactory, } from '../../../services/embeddable_test_samples'; import { coreMock, uiSettingsServiceMock } from '../../../../../../core/public/mocks'; +import { getStubPluginServices } from '../../../../../presentation_util/public'; let dashboardContainer: DashboardContainer | undefined; +const presentationUtil = getStubPluginServices(); function prepare(props?: Partial) { const { setup, doStart } = embeddablePluginMock.createInstance(); @@ -68,6 +70,7 @@ function prepare(props?: Partial) { } as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + presentationUtil, }; dashboardContainer = new DashboardContainer(initialInput, options); const defaultTestProps: DashboardGridProps = { @@ -96,7 +99,9 @@ test('renders DashboardGrid', () => { const { props, options } = prepare(); const component = mountWithIntl( - + + + ); const panelElements = component.find('EmbeddableChildPanel'); @@ -107,7 +112,9 @@ test('renders DashboardGrid with no visualizations', () => { const { props, options } = prepare(); const component = mountWithIntl( - + + + ); @@ -120,7 +127,9 @@ test('DashboardGrid removes panel when removed from container', () => { const { props, options } = prepare(); const component = mountWithIntl( - + + + ); @@ -137,7 +146,9 @@ test('DashboardGrid renders expanded panel', () => { const { props, options } = prepare(); const component = mountWithIntl( - + + + ); @@ -163,7 +174,9 @@ test('DashboardGrid unmount unsubscribes', async (done) => { const { props, options } = prepare(); const component = mountWithIntl( - + + + ); diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index ee9b633dfdcbf04..5d492c049fdad0c 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -19,11 +19,12 @@ import React from 'react'; import { Subscription } from 'rxjs'; import ReactGridLayout, { Layout } from 'react-grid-layout'; import { GridData } from '../../../../common'; -import { ViewMode, EmbeddableChildPanel } from '../../../services/embeddable'; +import { ViewMode } from '../../../services/embeddable'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; import { DashboardPanelState } from '../types'; import { withKibana } from '../../../services/kibana_react'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; +import { DashboardGridItem } from './dashboard_grid_item'; let lastValidGridSize = 0; @@ -123,9 +124,6 @@ interface PanelLayout extends Layout { class DashboardGridUi extends React.Component { private subscription?: Subscription; private mounted: boolean = false; - // A mapping of panelIndexes to grid items so we can set the zIndex appropriately on the last focused - // item. - private gridItems = {} as { [key: string]: HTMLDivElement | null }; constructor(props: DashboardGridProps) { super(props); @@ -222,13 +220,20 @@ class DashboardGridUi extends React.Component { } }; - public renderPanels() { - const { focusedPanelIndex, panels, expandedPanelId } = this.state; + public render() { + if (this.state.isLayoutInvalid) { + return null; + } + + const { container, kibana } = this.props; + const { focusedPanelIndex, panels, expandedPanelId, viewMode } = this.state; + const isViewMode = viewMode === ViewMode.VIEW; // Part of our unofficial API - need to render in a consistent order for plugins. const panelsInOrder = Object.keys(panels).map( (key: string) => panels[key] as DashboardPanelState ); + panelsInOrder.sort((panelA, panelB) => { if (panelA.gridData.y === panelB.gridData.y) { return panelA.gridData.x - panelB.gridData.x; @@ -237,55 +242,27 @@ class DashboardGridUi extends React.Component { } }); - return _.map(panelsInOrder, (panel) => { - const expandPanel = - expandedPanelId !== undefined && expandedPanelId === panel.explicitInput.id; - const hidePanel = expandedPanelId !== undefined && expandedPanelId !== panel.explicitInput.id; - const classes = classNames({ - // eslint-disable-next-line @typescript-eslint/naming-convention - 'dshDashboardGrid__item--expanded': expandPanel, - // eslint-disable-next-line @typescript-eslint/naming-convention - 'dshDashboardGrid__item--hidden': hidePanel, - }); - return ( -
{ - this.gridItems[panel.explicitInput.id] = reactGridItem; - }} - > - -
- ); - }); - } - - public render() { - if (this.state.isLayoutInvalid) { - return null; - } + const dashboardPanels = _.map(panelsInOrder, ({ explicitInput, type }) => ( + + )); - const { viewMode } = this.state; - const isViewMode = viewMode === ViewMode.VIEW; return ( - {this.renderPanels()} + {dashboardPanels} ); } diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid_item.tsx new file mode 100644 index 000000000000000..2054db6836dd542 --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid_item.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useRef, useEffect, FC } from 'react'; +import { EuiLoadingChart } from '@elastic/eui'; +import classNames from 'classnames'; + +import { EmbeddableChildPanel } from '../../../services/embeddable'; +import { useLabs } from '../../../services/presentation_util'; +import { DashboardPanelState } from '../types'; + +type PanelProps = Pick; +type DivProps = Pick, 'className' | 'style' | 'children'>; + +interface Props extends PanelProps, DivProps { + id: DashboardPanelState['explicitInput']['id']; + type: DashboardPanelState['type']; + focusedPanelId?: string; + expandedPanelId?: string; + key: string; + isRenderable?: boolean; +} + +const Item = React.forwardRef( + ( + { + container, + expandedPanelId, + focusedPanelId, + id, + PanelComponent, + type, + isRenderable = true, + // The props below are passed from ReactGridLayoutn and need to be merged with their counterparts. + // https://github.com/react-grid-layout/react-grid-layout/issues/1241#issuecomment-658306889 + children, + className, + style, + ...rest + }, + ref + ) => { + const expandPanel = expandedPanelId !== undefined && expandedPanelId === id; + const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id; + const classes = classNames({ + // eslint-disable-next-line @typescript-eslint/naming-convention + 'dshDashboardGrid__item--expanded': expandPanel, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'dshDashboardGrid__item--hidden': hidePanel, + }); + + return ( +
+ {isRenderable ? ( + <> + + {children} + + ) : ( +
+ +
+ )} +
+ ); + } +); + +export const ObservedItem: FC = (props: Props) => { + const [intersection, updateIntersection] = useState(); + const [isRenderable, setIsRenderable] = useState(false); + const panelRef = useRef(null); + + const observerRef = useRef( + new window.IntersectionObserver(([value]) => updateIntersection(value), { + root: panelRef.current, + }) + ); + + useEffect(() => { + const { current: currentObserver } = observerRef; + currentObserver.disconnect(); + const { current } = panelRef; + + if (current) { + currentObserver.observe(current); + } + + return () => currentObserver.disconnect(); + }, [panelRef]); + + useEffect(() => { + if (intersection?.isIntersecting && !isRenderable) { + setIsRenderable(true); + } + }, [intersection, isRenderable]); + + return ; +}; + +export const DashboardGridItem: FC = (props: Props) => { + const { isProjectEnabled } = useLabs(); + const isEnabled = isProjectEnabled('labs:dashboard:deferBelowFold'); + + return isEnabled ? : ; +}; diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index 093f09efa378030..690d1b177cdb5bb 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -26,8 +26,10 @@ import { ContactCardEmbeddableFactory, CONTACT_CARD_EMBEDDABLE, } from '../../../../../embeddable/public/lib/test_samples'; +import { getStubPluginServices } from '../../../../../presentation_util/public'; let dashboardContainer: DashboardContainer | undefined; +const presentationUtil = getStubPluginServices(); const ExitFullScreenButton = () =>
EXIT
; @@ -61,6 +63,7 @@ function getProps( uiActions: { getTriggerCompatibleActions: (() => []) as any, } as any, + presentationUtil, }; const input = getSampleDashboardInput({ @@ -94,7 +97,9 @@ test('renders DashboardViewport', () => { const component = mount( - + + + ); @@ -108,7 +113,9 @@ test('renders DashboardViewport with no visualizations', () => { const component = mount( - + + + ); @@ -124,7 +131,9 @@ test('renders DashboardEmptyScreen', () => { const component = mount( - + + + ); @@ -140,7 +149,9 @@ test('renders exit full screen button when in full screen mode', async () => { const component = mount( - + + + ); @@ -166,7 +177,9 @@ test('renders exit full screen button when in full screen mode and empty screen' const component = mount( - + + + ); @@ -190,7 +203,9 @@ test('DashboardViewport unmount unsubscribes', async (done) => { const component = mount( - + + + ); diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 02403999cd75c24..e1a62fe980f5523 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -13,6 +13,7 @@ import angular from 'angular'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import UseUnmount from 'react-use/lib/useUnmount'; +import { UI_SETTINGS } from '../../../common'; import { BaseVisType, VisTypeAlias } from '../../../../visualizations/public'; import { AddFromLibraryButton, @@ -30,6 +31,7 @@ import { SaveResult, showSaveModal, } from '../../services/saved_objects'; +import { LazyLabsFlyout, withSuspense } from '../../../../presentation_util/public'; import { NavAction } from '../../types'; import { DashboardSavedObject } from '../..'; @@ -76,6 +78,8 @@ export interface DashboardTopNavProps { viewMode: ViewMode; } +const Flyout = withSuspense(LazyLabsFlyout, null); + export function DashboardTopNav({ dashboardStateManager, clearUnsavedChanges, @@ -109,11 +113,13 @@ export function DashboardTopNav({ const [state, setState] = useState({ chromeIsVisible: false }); const [isSaveInProgress, setIsSaveInProgress] = useState(false); + const [isLabsShown, setIsLabsShown] = useState(false); const lensAlias = visualizations.getAliases().find(({ name }) => name === 'lens'); const quickButtonVisTypes = ['markdown', 'maps']; const stateTransferService = embeddable.getStateTransfer(); const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI); const trackUiMetric = usageCollection?.reportUiCounter.bind( usageCollection, @@ -489,6 +495,12 @@ export function DashboardTopNav({ dashboardCapabilities, }); } + + if (isLabsEnabled) { + actions[TopNavIds.LABS] = () => { + setIsLabsShown(!isLabsShown); + }; + } return actions; }, [ dashboardCapabilities, @@ -499,6 +511,8 @@ export function DashboardTopNav({ runSave, runQuickSave, share, + isLabsEnabled, + isLabsShown, ]); UseUnmount(() => { @@ -528,6 +542,7 @@ export function DashboardTopNav({ isNewDashboard: !savedDashboard.id, isDirty: dashboardStateManager.getIsDirty(timefilter), isSaveInProgress, + isLabsEnabled, }); const badges = unsavedChanges @@ -620,6 +635,9 @@ export function DashboardTopNav({ return ( <> + {isLabsEnabled && isLabsShown ? ( + setIsLabsShown(false)} /> + ) : null} {viewMode !== ViewMode.VIEW ? ( <> diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index da14f98468256be..a47c32750fdb0fb 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -26,16 +26,20 @@ export function getTopNavConfig( isNewDashboard: boolean; isDirty: boolean; isSaveInProgress?: boolean; + isLabsEnabled?: boolean; } ) { + const labs = options.isLabsEnabled ? [getLabsConfig(actions[TopNavIds.LABS])] : []; switch (dashboardMode) { case ViewMode.VIEW: return options.hideWriteControls ? [ + ...labs, getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), getShareConfig(actions[TopNavIds.SHARE]), ] : [ + ...labs, getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), getShareConfig(actions[TopNavIds.SHARE]), getCloneConfig(actions[TopNavIds.CLONE]), @@ -44,6 +48,7 @@ export function getTopNavConfig( case ViewMode.EDIT: const disableButton = options.isSaveInProgress; const navItems: TopNavMenuData[] = [ + ...labs, getOptionsConfig(actions[TopNavIds.OPTIONS], disableButton), getShareConfig(actions[TopNavIds.SHARE], disableButton), ]; @@ -91,6 +96,20 @@ function getFullScreenConfig(action: NavAction) { }; } +function getLabsConfig(action: NavAction) { + return { + id: 'labs', + label: i18n.translate('dashboard.topNav.labsButtonAriaLabel', { + defaultMessage: 'labs', + }), + description: i18n.translate('dashboard.topNav.labsConfigDescription', { + defaultMessage: 'Labs', + }), + testId: 'dashboardLabs', + run: action, + }; +} + /** * @returns {kbnTopNavConfig} */ diff --git a/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts b/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts index ee3d08e2330ae9d..8f2f580dbdd3c19 100644 --- a/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts +++ b/src/plugins/dashboard/public/application/top_nav/top_nav_ids.ts @@ -15,4 +15,5 @@ export const TopNavIds = { ENTER_EDIT_MODE: 'enterEditMode', CLONE: 'clone', FULL_SCREEN: 'fullScreenMode', + LABS: 'labs', }; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 0c4ef8c58f949d9..230918399d88f85 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -145,15 +145,7 @@ export class DashboardPlugin public setup( core: CoreSetup, - { - share, - uiActions, - embeddable, - home, - urlForwarding, - data, - usageCollection, - }: DashboardSetupDependencies + { share, embeddable, home, urlForwarding, data, usageCollection }: DashboardSetupDependencies ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); const startServices = core.getStartServices(); @@ -208,6 +200,7 @@ export class DashboardPlugin inspector: deps.inspector, http: coreStart.http, ExitFullScreenButton, + presentationUtil: deps.presentationUtil, }; }; diff --git a/src/plugins/dashboard/public/services/presentation_util.ts b/src/plugins/dashboard/public/services/presentation_util.ts index d3e6c1ebe9eec8e..17bc97db9ac6335 100644 --- a/src/plugins/dashboard/public/services/presentation_util.ts +++ b/src/plugins/dashboard/public/services/presentation_util.ts @@ -10,4 +10,5 @@ export { PresentationUtilPluginStart, LazyDashboardPicker, withSuspense, + useLabs, } from '../../../presentation_util/public'; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index fbed98a882b0ab7..6c1eea29f5297fb 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -22,6 +22,7 @@ import { EmbeddableSetup } from '../../embeddable/server'; import { UsageCollectionSetup } from '../../usage_collection/server'; import { registerDashboardUsageCollector } from './usage/register_collector'; import { dashboardPersistableStateServiceFactory } from './embeddable/dashboard_container_embeddable_factory'; +import { getUISettings } from './ui_settings'; interface SetupDeps { embeddable: EmbeddableSetup; @@ -54,6 +55,8 @@ export class DashboardPlugin dashboardPersistableStateServiceFactory(plugins.embeddable) ); + core.uiSettings.register(getUISettings()); + return {}; } diff --git a/src/plugins/dashboard/server/ui_settings.ts b/src/plugins/dashboard/server/ui_settings.ts new file mode 100644 index 000000000000000..34cfff0e4ef4732 --- /dev/null +++ b/src/plugins/dashboard/server/ui_settings.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { SETTING_CATEGORY } from '../../../../src/plugins/presentation_util/server'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { UI_SETTINGS } from '../common'; + +/** + * uiSettings definitions for Dashboard. + */ +export const getUISettings = (): Record> => ({ + [UI_SETTINGS.ENABLE_LABS_UI]: { + name: i18n.translate('dashboard.labs.enableUI', { + defaultMessage: 'Enable labs button in Dashboard', + }), + description: i18n.translate('dashboard.labs.enableUnifiedToolbarProjectDescription', { + defaultMessage: + 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable experimental features in Dashboard.', + }), + value: false, + type: 'boolean', + schema: schema.boolean(), + category: [SETTING_CATEGORY], + requiresPageReload: true, + }, +}); diff --git a/src/plugins/data/common/es_query/filters/range_filter.test.ts b/src/plugins/data/common/es_query/filters/range_filter.test.ts index c7ff84f61d0fcb1..bb7ecc09ebc349a 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.test.ts @@ -64,6 +64,29 @@ describe('Range filter builder', () => { }); }); + it('should convert strings to numbers if the field is scripted and type number', () => { + const field = getField('script number'); + + expect(buildRangeFilter(field, { gte: '1', lte: '3' }, indexPattern)).toEqual({ + meta: { + field: 'script number', + index: 'id', + params: {}, + }, + script: { + script: { + lang: 'expression', + source: '(' + field!.script + ')>=gte && (' + field!.script + ')<=lte', + params: { + value: '>=1 <=3', + gte: 1, + lte: 3, + }, + }, + }, + }); + }); + it('should wrap painless scripts in comparator lambdas', () => { const field = getField('script date'); const expected = diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index ba9055e26dbda8d..fb8426655583ee3 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -138,7 +138,10 @@ export const buildRangeFilter = ( }; export const getRangeScript = (field: IFieldType, params: RangeFilterParams) => { - const knownParams = pickBy(params, (val, key: any) => key in operators); + const knownParams = mapValues( + pickBy(params, (val, key: any) => key in operators), + (value) => (field.type === 'number' && typeof value === 'string' ? parseFloat(value) : value) + ); let script = map( knownParams, (val: any, key: string) => '(' + field.script + ')' + get(operators, key) + key diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index fa8f6c3bc1dc83c..565dd6d9269488a 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -8,6 +8,10 @@ import { FieldSpec, IFieldSubType, IndexPattern } from '../..'; +/** + * @deprecated + * Use IndexPatternField or FieldSpec instead + */ export interface IFieldType { name: string; type: string; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index c906b809b08c4ca..0fcdea1a878ebcf 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -25,8 +25,9 @@ export interface RuntimeField { } /** + * @deprecated * IIndexPattern allows for an IndexPattern OR an index pattern saved object - * too ambiguous, should be avoided + * Use IndexPattern or IndexPatternSpec instead */ export interface IIndexPattern { fields: IFieldType[]; 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 c99676d71957a32..5bcd614d9debfe4 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range.ts @@ -18,7 +18,7 @@ import { KBN_FIELD_TYPES } from '../../../../common'; import { BaseAggParams } from '../types'; const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', { - defaultMessage: 'IPv4 Range', + defaultMessage: 'IP Range', }); export enum IP_RANGE_TYPES { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts index 04363ee79228945..b0466b78529e741 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts @@ -9,56 +9,47 @@ import { CidrMask } from './cidr_mask'; describe('CidrMask', () => { - test('should throw errors with invalid CIDR masks', () => { - expect( - () => - // @ts-ignore - new CidrMask() - ).toThrowError(); - - expect(() => new CidrMask('')).toThrowError(); - expect(() => new CidrMask('hello, world')).toThrowError(); - expect(() => new CidrMask('0.0.0.0')).toThrowError(); - expect(() => new CidrMask('0.0.0.0/0')).toThrowError(); - expect(() => new CidrMask('0.0.0.0/33')).toThrowError(); - expect(() => new CidrMask('256.0.0.0/32')).toThrowError(); - expect(() => new CidrMask('0.0.0.0/32/32')).toThrowError(); - expect(() => new CidrMask('1.2.3/1')).toThrowError(); - expect(() => new CidrMask('0.0.0.0/123d')).toThrowError(); + describe('constructor', () => { + it.each` + mask + ${''} + ${'hello, world'} + ${'0.0.0.0'} + ${'0.0.0.0/33'} + ${'256.0.0.0/32'} + ${'0.0.0.0/32/32'} + ${'0.0.0.0/123d'} + ${'::1'} + ${'::1/129'} + ${'fffff::/128'} + ${'ffff::/128/128'} + `('should throw an error on $mask', ({ mask }) => { + expect(() => new CidrMask(mask)).toThrowError(); + }); }); - test('should correctly grab IP address and prefix length', () => { - let mask = new CidrMask('0.0.0.0/1'); - expect(mask.initialAddress.toString()).toBe('0.0.0.0'); - expect(mask.prefixLength).toBe(1); - - mask = new CidrMask('128.0.0.1/31'); - expect(mask.initialAddress.toString()).toBe('128.0.0.1'); - expect(mask.prefixLength).toBe(31); + describe('toString', () => { + it.each` + mask | expected + ${'192.168.1.1/24'} | ${'192.168.1.1/24'} + ${'192.168.257/32'} | ${'192.168.1.1/32'} + ${'ffff:0:0:0:0:0:0:0/128'} | ${'ffff::/128'} + `('should format $mask as $expected', ({ mask, expected }) => { + expect(new CidrMask(mask).toString()).toBe(expected); + }); }); - test('should calculate a range of IP addresses', () => { - let mask = new CidrMask('0.0.0.0/1'); - let range = mask.getRange(); - expect(range.from.toString()).toBe('0.0.0.0'); - expect(range.to.toString()).toBe('127.255.255.255'); - - mask = new CidrMask('1.2.3.4/2'); - range = mask.getRange(); - expect(range.from.toString()).toBe('0.0.0.0'); - expect(range.to.toString()).toBe('63.255.255.255'); - - mask = new CidrMask('67.129.65.201/27'); - range = mask.getRange(); - expect(range.from.toString()).toBe('67.129.65.192'); - expect(range.to.toString()).toBe('67.129.65.223'); - }); - - test('toString()', () => { - let mask = new CidrMask('.../1'); - expect(mask.toString()).toBe('0.0.0.0/1'); - - mask = new CidrMask('128.0.0.1/31'); - expect(mask.toString()).toBe('128.0.0.1/31'); + describe('getRange', () => { + it.each` + mask | from | to + ${'0.0.0.0/0'} | ${'0.0.0.0'} | ${'255.255.255.255'} + ${'0.0.0.0/1'} | ${'0.0.0.0'} | ${'127.255.255.255'} + ${'1.2.3.4/2'} | ${'0.0.0.0'} | ${'63.255.255.255'} + ${'67.129.65.201/27'} | ${'67.129.65.192'} | ${'67.129.65.223'} + ${'::/1'} | ${'::'} | ${'7fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'} + ${'8000::/1'} | ${'8000::'} | ${'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'} + `('should return $from-$to for $mask', ({ mask, from, to }) => { + expect(new CidrMask(mask).getRange()).toEqual({ from, to }); + }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts index 1a1c9b8157838c0..93ccbffaeb89d46 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts @@ -6,42 +6,60 @@ * Side Public License, v 1. */ -import { Ipv4Address } from '../../utils'; - -const NUM_BITS = 32; - -function throwError(mask: string) { - throw Error('Invalid CIDR mask: ' + mask); -} +import ipaddr from 'ipaddr.js'; +import { IpAddress } from '../../utils'; export class CidrMask { - public readonly initialAddress: Ipv4Address; - public readonly prefixLength: number; + private address: number[]; + private netmask: number; - constructor(mask: string) { - const splits = mask.split('/'); - if (splits.length !== 2) { - throwError(mask); - } - this.initialAddress = new Ipv4Address(splits[0]); - this.prefixLength = Number(splits[1]); - if (isNaN(this.prefixLength) || this.prefixLength < 1 || this.prefixLength > NUM_BITS) { - throwError(mask); + constructor(cidr: string) { + try { + const [address, netmask] = ipaddr.parseCIDR(cidr); + + this.address = address.toByteArray(); + this.netmask = netmask; + } catch { + throw Error('Invalid CIDR mask: ' + cidr); } } - public getRange() { - const variableBits = NUM_BITS - this.prefixLength; - // eslint-disable-next-line no-bitwise - const fromAddress = ((this.initialAddress.valueOf() >> variableBits) << variableBits) >>> 0; // >>> 0 coerces to unsigned - const numAddresses = Math.pow(2, variableBits); + private getBroadcastAddress() { + /* eslint-disable no-bitwise */ + const netmask = (1n << BigInt(this.address.length * 8 - this.netmask)) - 1n; + const broadcast = this.address.map((byte, index) => { + const offset = BigInt(this.address.length - index - 1) * 8n; + const mask = Number((netmask >> offset) & 255n); + + return byte | mask; + }); + /* eslint-enable no-bitwise */ + + return new IpAddress(broadcast).toString(); + } + + private getNetworkAddress() { + /* eslint-disable no-bitwise */ + const netmask = (1n << BigInt(this.address.length * 8 - this.netmask)) - 1n; + const network = this.address.map((byte, index) => { + const offset = BigInt(this.address.length - index - 1) * 8n; + const mask = Number((netmask >> offset) & 255n) ^ 255; + + return byte & mask; + }); + /* eslint-enable no-bitwise */ + + return new IpAddress(network).toString(); + } + + getRange() { return { - from: new Ipv4Address(fromAddress).toString(), - to: new Ipv4Address(fromAddress + numAddresses - 1).toString(), + from: this.getNetworkAddress(), + to: this.getBroadcastAddress(), }; } - public toString() { - return this.initialAddress.toString() + '/' + this.prefixLength; + toString() { + return `${new IpAddress(this.address)}/${this.netmask}`; } } diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts index 72d59af037d90e5..4b3ec90bb2cc2b2 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts @@ -12,7 +12,7 @@ import dateMath, { Unit } from '@elastic/datemath'; import { parseEsInterval } from '../../../utils'; const unitsDesc = dateMath.unitsDesc; -const largeMax = unitsDesc.indexOf('M'); +const largeMax = unitsDesc.indexOf('w'); export interface EsInterval { expression: string; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index 6fbaddb09b2262c..fd3f861dce07e4e 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; import { autoInterval } from '../../_interval_options'; +import { InvalidEsCalendarIntervalError } from '../../../utils'; describe('TimeBuckets', () => { const timeBucketConfig: TimeBucketsConfig = { @@ -137,4 +138,14 @@ describe('TimeBuckets', () => { const format = timeBuckets.getScaledDateFormat(); expect(format).toEqual('HH:mm'); }); + + test('allows days but throws error on weeks', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + timeBuckets.setInterval('14d'); + const interval = timeBuckets.getInterval(false); + expect(interval.esUnit).toEqual('d'); + + timeBuckets.setInterval('2w'); + expect(() => timeBuckets.getInterval(false)).toThrow(InvalidEsCalendarIntervalError); + }); }); diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index c92653e84323365..f1625070b6f75f3 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -11,7 +11,7 @@ export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histog export { getDateHistogramMetaDataByDatatableColumn } from './get_date_histogram_meta'; export * from './date_interval_utils'; export * from './get_format_with_aggs'; -export * from './ipv4_address'; +export * from './ip_address'; export * from './prop_filter'; export * from './to_angular_json'; export * from './infer_time_zone'; diff --git a/src/plugins/data/common/search/aggs/utils/ip_address.test.ts b/src/plugins/data/common/search/aggs/utils/ip_address.test.ts new file mode 100644 index 000000000000000..966408cf6fe27e7 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/ip_address.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IpAddress } from './ip_address'; + +describe('IpAddress', () => { + describe('constructor', () => { + it.each` + address + ${''} + ${'hello, world'} + ${'256.0.0.0'} + ${'-1.0.0.0'} + ${Number.MAX_SAFE_INTEGER} + ${'fffff::'} + ${'ffff:0:0:0:0:0:0:0:0'} + `('should throw an error on $address', ({ address }) => { + expect(() => new IpAddress(address)).toThrowError(); + }); + + it.each` + address | expected + ${'192.168.257'} | ${'192.168.1.1'} + ${2116932386} | ${'126.45.211.34'} + ${'126.45.211.34'} | ${'126.45.211.34'} + ${[126, 45, 211, 34]} | ${'126.45.211.34'} + ${'ffff:0:0:0:0:0:0:0'} | ${'ffff::'} + ${'ffff::'} | ${'ffff::'} + ${[0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]} | ${'ffff::'} + `('should parse $address', ({ address, expected }) => { + expect(new IpAddress(address).toString()).toBe(expected); + }); + }); + + describe('valueOf', () => { + it.each` + address | expected + ${'0.0.0.0'} | ${'0'} + ${'0.0.0.1'} | ${'1'} + ${'126.45.211.34'} | ${'2116932386'} + ${'ffff::'} | ${'340277174624079928635746076935438991360'} + `( + 'should return $expected as a decimal representation of $address', + ({ address, expected }) => { + expect(new IpAddress(address).valueOf().toString()).toBe(expected); + } + ); + }); + + describe('toString()', () => { + it.each` + address | expected + ${'0.000.00000.1'} | ${'0.0.0.1'} + ${'192.168.257'} | ${'192.168.1.1'} + ${'ffff:0:0:0:0:0:0:0'} | ${'ffff::'} + ${'0:0:0:0:0:0:0:ffff'} | ${'::ffff'} + ${'f:0:0:0:0:0:0:f'} | ${'f::f'} + `('should serialize $address as $expected', ({ address, expected }) => { + expect(new IpAddress(address).toString()).toBe(expected); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/ip_address.ts b/src/plugins/data/common/search/aggs/utils/ip_address.ts new file mode 100644 index 000000000000000..2fffbc468046f14 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/ip_address.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import ipaddr, { IPv4, IPv6 } from 'ipaddr.js'; + +function isIPv6(value: IPv4 | IPv6): value is IPv6 { + return value.kind() === 'ipv6'; +} + +export class IpAddress { + private value: IPv4 | IPv6; + + constructor(ipAddress: string | number | number[]) { + try { + this.value = Array.isArray(ipAddress) + ? ipaddr.fromByteArray(ipAddress) + : ipaddr.parse(`${ipAddress}`); + } catch { + throw Error('Invalid IP address: ' + ipAddress); + } + } + + toString() { + if (isIPv6(this.value)) { + return this.value.toRFC5952String(); + } + + return this.value.toString(); + } + + valueOf(): number | bigint { + const value = this.value + .toByteArray() + .reduce((result, octet) => result * 256n + BigInt(octet), 0n); + + if (value > Number.MAX_SAFE_INTEGER) { + return value; + } + + return Number(value); + } +} diff --git a/src/plugins/data/common/search/aggs/utils/ipv4_address.test.ts b/src/plugins/data/common/search/aggs/utils/ipv4_address.test.ts deleted file mode 100644 index 4be406f54390f13..000000000000000 --- a/src/plugins/data/common/search/aggs/utils/ipv4_address.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; -import { Ipv4Address } from './ipv4_address'; - -describe('Ipv4Address', () => { - it('should throw errors with invalid IP addresses', () => { - // @ts-ignore - expect(() => new Ipv4Address()).to.throwError(); - - expect(() => new Ipv4Address('')).to.throwError(); - - expect(() => new Ipv4Address('hello, world')).to.throwError(); - - expect(() => new Ipv4Address('0.0.0')).to.throwError(); - - expect(() => new Ipv4Address('256.0.0.0')).to.throwError(); - - expect(() => new Ipv4Address('-1.0.0.0')).to.throwError(); - - expect(() => new Ipv4Address(Number.MAX_SAFE_INTEGER)).to.throwError(); - }); - - it('should allow creation with an integer or string', () => { - expect(new Ipv4Address(2116932386).toString()).to.be( - new Ipv4Address('126.45.211.34').toString() - ); - }); - - it('should correctly calculate the decimal representation of an IP address', () => { - let ipAddress = new Ipv4Address('0.0.0.0'); - expect(ipAddress.valueOf()).to.be(0); - - ipAddress = new Ipv4Address('0.0.0.1'); - expect(ipAddress.valueOf()).to.be(1); - - ipAddress = new Ipv4Address('126.45.211.34'); - expect(ipAddress.valueOf()).to.be(2116932386); - }); - - it('toString()', () => { - let ipAddress = new Ipv4Address('0.000.00000.1'); - expect(ipAddress.toString()).to.be('0.0.0.1'); - - ipAddress = new Ipv4Address('123.123.123.123'); - expect(ipAddress.toString()).to.be('123.123.123.123'); - }); -}); diff --git a/src/plugins/data/common/search/aggs/utils/ipv4_address.ts b/src/plugins/data/common/search/aggs/utils/ipv4_address.ts deleted file mode 100644 index aa6588349c9db22..000000000000000 --- a/src/plugins/data/common/search/aggs/utils/ipv4_address.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const NUM_BYTES = 4; -const BYTE_SIZE = 256; - -function throwError(ipAddress: string | number) { - throw Error('Invalid IPv4 address: ' + ipAddress); -} - -function isIntegerInRange(integer: number, min: number, max: number) { - return ( - !isNaN(integer as number) && integer >= min && integer < max && (integer as number) % 1 === 0 - ); -} - -export class Ipv4Address { - private value: number; - - constructor(ipAddress: string | number) { - if (typeof ipAddress === 'string') { - this.value = 0; - - const bytes = ipAddress.split('.'); - if (bytes.length !== NUM_BYTES) { - throwError(ipAddress); - } - - for (let i = 0; i < bytes.length; i++) { - const byte = Number(bytes[i]); - if (!isIntegerInRange(byte, 0, BYTE_SIZE)) { - throwError(ipAddress); - } - this.value += Math.pow(BYTE_SIZE, NUM_BYTES - 1 - i) * byte; - } - } else { - this.value = ipAddress; - } - - if (!isIntegerInRange(this.value, 0, Math.pow(BYTE_SIZE, NUM_BYTES))) { - throwError(ipAddress); - } - } - - public toString() { - let value = this.value; - const bytes = []; - for (let i = 0; i < NUM_BYTES; i++) { - bytes.unshift(value % 256); - value = Math.floor(value / 256); - } - return bytes.join('.'); - } - - public valueOf() { - return this.value; - } -} 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 ef3e020747f7a0e..7c0473077d18240 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 @@ -27,7 +27,7 @@ const mockSource2 = { excludes: ['bar-*'] }; const indexPattern = ({ title: 'foo', - fields: [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }], + fields: [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }, { name: '_id' }], getComputedFields, getSourceFiltering: () => mockSource, } as unknown) as IndexPattern; @@ -68,7 +68,7 @@ describe('SearchSource', () => { beforeEach(() => { const getConfigMock = jest .fn() - .mockImplementation((param) => param === 'metaFields' && ['_type', '_source']) + .mockImplementation((param) => param === 'metaFields' && ['_type', '_source', '_id']) .mockName('getConfig'); mockSearchMethod = jest @@ -458,6 +458,28 @@ describe('SearchSource', () => { expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); }); + test('excludes metafields from the request', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: [], + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); + + const request = searchSource.getSearchRequestBody(); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); + + searchSource.setField('fields', ['foo-bar', 'foo--bar', 'field1', 'field2']); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); + + searchSource.removeField('fields'); + searchSource.setField('fieldsFromSource', ['foo-bar', 'foo--bar', 'field1', 'field2']); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); + }); + test('returns all scripted fields when one fields entry is *', async () => { searchSource.setField('index', ({ ...indexPattern, 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 5130224329ba2bc..f35d2d47f1bf471 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -682,6 +682,7 @@ export class SearchSource { searchRequest.body = searchRequest.body || {}; const { body, index, query, filters, highlightAll } = searchRequest; searchRequest.indexType = this.getIndexType(index); + const metaFields = getConfig(UI_SETTINGS.META_FIELDS); // get some special field types from the index pattern const { docvalueFields, scriptFields, storedFields, runtimeFields } = index @@ -712,7 +713,7 @@ export class SearchSource { body._source = sourceFilters; } - const filter = fieldWildcardFilter(body._source.excludes, getConfig(UI_SETTINGS.META_FIELDS)); + const filter = fieldWildcardFilter(body._source.excludes, metaFields); // also apply filters to provided fields & default docvalueFields body.fields = body.fields.filter((fld: SearchFieldValue) => filter(this.getFieldName(fld))); fieldsFromSource = fieldsFromSource.filter((fld: SearchFieldValue) => @@ -793,17 +794,21 @@ export class SearchSource { const field2Name = this.getFieldName(fld2); return field1Name === field2Name; } - ).map((fld: SearchFieldValue) => { - const fieldName = this.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] - : this.getFieldFromDocValueFieldsOrIndexPattern(docvaluesIndex, fld, index); - } - return fld; - }); + ) + .filter((fld: SearchFieldValue) => { + return !metaFields.includes(this.getFieldName(fld)); + }) + .map((fld: SearchFieldValue) => { + const fieldName = this.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] + : this.getFieldFromDocValueFieldsOrIndexPattern(docvaluesIndex, fld, index); + } + return fld; + }); } } else { body.fields = filteredDocvalueFields; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 30667d59eff84d8..14ae24a2a5626b9 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -105,4 +105,11 @@ describe('createFiltersFromValueClick', () => { expect(rangeFilter.range.bytes.lt).toEqual(2078); } }); + + test('handles non-unique filters', async () => { + const [point] = dataPoints; + const filters = await createFiltersFromValueClickAction({ data: [point, point] }); + + expect(filters.length).toEqual(1); + }); }); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index fb8240a0cdc27f9..13cb9ba419bf9dd 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import _ from 'lodash'; import { Datatable } from '../../../../../plugins/expressions/public'; import { esFilters, Filter } from '../../../public'; import { getIndexPatterns, getSearchService } from '../../../public/services'; @@ -140,5 +141,7 @@ export const createFiltersFromValueClickAction = async ({ }) ); - return esFilters.mapAndFlattenFilters(filters); + return _.uniqWith(esFilters.mapAndFlattenFilters(filters), (a, b) => + esFilters.compareFilters(a, b, esFilters.COMPARE_ALL_OPTIONS) + ); }; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index f2a61e94a07d926..ba873952c9841f8 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -305,7 +305,7 @@ import { dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, - Ipv4Address, + IpAddress, isValidEsInterval, isValidInterval, parseEsInterval, @@ -411,7 +411,7 @@ export const search = { intervalOptions, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, - Ipv4Address, + IpAddress, isDateHistogramBucketAggConfig, // TODO: remove in build_pipeline refactor isNumberType, isStringType, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d4f0ccfe810c6fb..8561d7bf8d6f5df 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1178,7 +1178,7 @@ export interface IFieldSubType { // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export interface IFieldType { // (undocumented) aggregatable?: boolean; @@ -1222,7 +1222,7 @@ export interface IFieldType { // Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated (undocumented) export interface IIndexPattern { // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts // @@ -2299,7 +2299,7 @@ export const search: { })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; - Ipv4Address: typeof Ipv4Address; + IpAddress: typeof IpAddress; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; isNumberType: (agg: import("../common").AggConfig) => boolean; isStringType: (agg: import("../common").AggConfig) => boolean; @@ -2737,7 +2737,7 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts index 1a29a57ff058ab9..0daaf804e7b40f5 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts @@ -174,4 +174,22 @@ describe('Generate filters', () => { [FIELD.name]: ANOTHER_PHRASE, }); }); + + it('should use only distinct values', () => { + const ANOTHER_PHRASE = 'another-value'; + const filters = generateFilters( + mockFilterManager, + FIELD, + [PHRASE_VALUE, ANOTHER_PHRASE, PHRASE_VALUE, ANOTHER_PHRASE], + '', + INDEX_NAME + ); + expect(filters).toHaveLength(2); + expect((filters[0] as PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: PHRASE_VALUE, + }); + expect((filters[1] as PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: ANOTHER_PHRASE, + }); + }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index 9a52a02edcd4e6f..0a4998a15952309 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -71,7 +71,7 @@ export function generateFilters( operation: string, index: string ): Filter[] { - values = Array.isArray(values) ? values : [values]; + values = Array.isArray(values) ? _.uniq(values) : [values]; const fieldObj = (_.isObject(field) ? field : { diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/filter_editor.test.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/filter_editor.test.tsx new file mode 100644 index 000000000000000..4760968b655390e --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/filter_editor.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { registerTestBed, TestBed } from '@kbn/test/jest'; +import { FilterEditor, Props } from '.'; +import React from 'react'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiCodeEditor: (props: any) => ( + { + props.onChange(eve.target.value); + }} + /> + ), + }; +}); + +describe('', () => { + describe('writing query dsl', () => { + let testBed: TestBed; + + beforeEach(async () => { + const defaultProps: Omit = { + filter: { + meta: { + type: 'phase', + } as any, + }, + indexPatterns: [], + onCancel: jest.fn(), + onSubmit: jest.fn(), + }; + testBed = await registerTestBed(FilterEditor, { defaultProps })(); + }); + + it('requires a non-empty JSON object', async () => { + const { exists, find } = testBed; + + expect(exists('customEditorInput')).toBe(true); + + find('customEditorInput').simulate('change', { + target: { value: '{ }' }, + }); + expect(find('saveFilter').props().disabled).toBe(true); + + find('customEditorInput').simulate('change', { + target: { value: '{' }, // bad JSON + }); + expect(find('saveFilter').props().disabled).toBe(true); + + find('customEditorInput').simulate('change', { + target: { value: '{ "something": "here" }' }, + }); + + expect(find('saveFilter').props().disabled).toBe(false); + }); + }); +}); diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index d2f04228ed39621..2b8978a125bcacb 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -48,7 +48,7 @@ import { getFilterParams, } from '../../../../common'; -interface Props { +export interface Props { filter: Filter; indexPatterns: IIndexPattern[]; onSubmit: (filter: Filter) => void; @@ -333,6 +333,7 @@ class FilterEditorUI extends Component { mode="json" width="100%" height="250px" + data-test-subj="customEditorInput" /> ); @@ -415,7 +416,8 @@ class FilterEditorUI extends Component { if (isCustomEditorOpen) { try { - return Boolean(JSON.parse(queryDsl)); + const queryDslJson = JSON.parse(queryDsl); + return Object.keys(queryDslJson).length > 0; } catch (e) { return false; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts index d6e5b57f5878bec..e15f667128c4f21 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -12,7 +12,7 @@ import { isFilterable, IIndexPattern, IFieldType, - Ipv4Address, + IpAddress, Filter, FieldFilter, } from '../../../../../common'; @@ -44,7 +44,7 @@ export function validateParams(params: any, type: string) { return Boolean(typeof params === 'string' && moment && moment.isValid()); case 'ip': try { - return Boolean(new Ipv4Address(params)); + return Boolean(new IpAddress(params)); } catch (e) { return false; } diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 9c84b9b73d5e501..1608b68ed01b22f 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -10,7 +10,6 @@ import { compact } from 'lodash'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; import { EuiIconProps } from '@elastic/eui'; @@ -100,8 +99,6 @@ class SearchBarUI extends Component { private services = this.props.kibana.services; private savedQueryService = this.services.data.query.savedQueries; - public filterBarRef: Element | null = null; - public filterBarWrapperRef: Element | null = null; public static getDerivedStateFromProps(nextProps: SearchBarProps, prevState: State) { if (isEqual(prevState.currentProps, nextProps)) { @@ -212,19 +209,6 @@ class SearchBarUI extends Component { ); } - public setFilterBarHeight = () => { - requestAnimationFrame(() => { - const height = - this.filterBarRef && this.state.isFiltersVisible ? this.filterBarRef.clientHeight : 0; - if (this.filterBarWrapperRef) { - this.filterBarWrapperRef.setAttribute('style', `height: ${height}px`); - } - }); - }; - - // member-ordering rules conflict with use-before-declaration rules - public ro = new ResizeObserver(this.setFilterBarHeight); - public onSave = async (savedQueryMeta: SavedQueryMeta, saveAsNew = false) => { if (!this.state.query) return; @@ -352,20 +336,6 @@ class SearchBarUI extends Component { } }; - public componentDidMount() { - if (this.filterBarRef) { - this.setFilterBarHeight(); - this.ro.observe(this.filterBarRef); - } - } - - public componentDidUpdate() { - if (this.filterBarRef) { - this.setFilterBarHeight(); - this.ro.unobserve(this.filterBarRef); - } - } - public render() { const savedQueryManagement = this.state.query && this.props.onClearSavedQuery && ( { 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, }); filterBar = ( -
{ - this.filterBarWrapperRef = node; - }} - className={filterGroupClasses} - > -
{ - this.filterBarRef = node; - }} - > - -
+
+
); } diff --git a/src/plugins/data/server/field_formats/field_formats_service.ts b/src/plugins/data/server/field_formats/field_formats_service.ts index 70bb93f13ca5fff..fa94e5dcef9cf89 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -33,8 +33,9 @@ export class FieldFormatsService { return { fieldFormatServiceFactory: async (uiSettings: IUiSettingsClient) => { const fieldFormatsRegistry = new FieldFormatsRegistry(); - const uiConfigs = await uiSettings.getAll(); + const coreUiConfigs = await uiSettings.getAll(); const registeredUiSettings = uiSettings.getRegistered(); + const uiConfigs = { ...coreUiConfigs }; Object.keys(registeredUiSettings).forEach((key) => { if (has(uiConfigs, key) && registeredUiSettings[key].type === 'json') { diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index c4e132e33fc3b52..e782fc0d4ffeaa2 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -170,7 +170,7 @@ import { dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, - Ipv4Address, + IpAddress, isValidEsInterval, isValidInterval, parseEsInterval, @@ -247,7 +247,7 @@ export const search = { intervalOptions, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, - Ipv4Address, + IpAddress, isNumberType, isStringType, isType, diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index e7f5005e7e8378e..cec4b9a2dbf9f6f 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -39,7 +39,7 @@ export const enhancedEsSearchStrategyProvider = ( legacyConfig$: Observable, logger: Logger, usage?: SearchUsage -): ISearchStrategy => { +): ISearchStrategy => { async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) { try { await esClient.asCurrentUser.asyncSearch.delete({ id }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index e66eaab672e1c17..ffdff2e33cf9cdb 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -692,7 +692,7 @@ export interface IFieldSubType { // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export interface IFieldType { // (undocumented) aggregatable?: boolean; @@ -993,7 +993,7 @@ export class IndexPatternsServiceProvider implements Plugin_3, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1263,7 +1263,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1341,7 +1341,7 @@ export const search: { })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; - Ipv4Address: typeof Ipv4Address; + IpAddress: typeof IpAddress; isNumberType: (agg: import("../common").AggConfig) => boolean; isStringType: (agg: import("../common").AggConfig) => boolean; isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean; @@ -1544,7 +1544,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:261:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts 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 58ddf1eb7ba25b2..1d6956fc809201c 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 @@ -6,27 +6,17 @@ * Side Public License, v 1. */ -import { find, template } from 'lodash'; +import { find } from 'lodash'; import $ from 'jquery'; import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import cellTemplateHtml from '../components/table_row/cell.html'; -import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; import { getServices } from '../../../../kibana_services'; import { getContextUrl } from '../../../helpers/get_context_url'; import { formatRow, formatTopLevelObject } from '../../helpers'; - -const TAGS_WITH_WS = />\s+<'); -} +import { truncateByHeight } from './table_row/truncate_by_height'; +import { cell } from './table_row/cell'; // guesstimate at the minimum number of chars wide cells in the table should be const MIN_LINE_LENGTH = 20; @@ -37,9 +27,6 @@ interface LazyScope extends ng.IScope { } export function createTableRowDirective($compile: ng.ICompileService) { - const cellTemplate = template(noWhiteSpace(cellTemplateHtml)); - const truncateByHeightTemplate = template(noWhiteSpace(truncateByHeightTemplateHtml)); - return { restrict: 'A', scope: { @@ -133,7 +120,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { const hideTimeColumn = getServices().uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); if (indexPattern.timeFieldName && !hideTimeColumn) { newHtmls.push( - cellTemplate({ + cell({ timefield: true, formatted: _displayField(row, indexPattern.timeFieldName), filterable: mapping(indexPattern.timeFieldName).filterable && $scope.filter, @@ -146,7 +133,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { const formatted = formatRow(row, indexPattern); newHtmls.push( - cellTemplate({ + cell({ timefield: false, sourcefield: true, formatted, @@ -164,7 +151,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { }) ); newHtmls.push( - cellTemplate({ + cell({ timefield: false, sourcefield: true, formatted: formatTopLevelObject(row, innerColumns, indexPattern), @@ -174,7 +161,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { ); } else { newHtmls.push( - cellTemplate({ + cell({ timefield: false, sourcefield: column === '_source', formatted: _displayField(row, column, true), @@ -191,8 +178,8 @@ export function createTableRowDirective($compile: ng.ICompileService) { const $cell = $cells.eq(i); if ($cell.data('discover:html') === html) return; - const reuse = find($cells.slice(i + 1), (cell) => { - return $.data(cell, 'discover:html') === html; + const reuse = find($cells.slice(i + 1), (c) => { + return $.data(c, 'discover:html') === html; }); const $target = reuse ? $(reuse).detach() : $(html); @@ -231,7 +218,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { const text = indexPattern.formatField(row, fieldName); if (truncate && text.length > MIN_LINE_LENGTH) { - return truncateByHeightTemplate({ + return truncateByHeight({ body: text, }); } diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html deleted file mode 100644 index 0d17c2ca94cac21..000000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html +++ /dev/null @@ -1,37 +0,0 @@ -<% -var attributes = ''; -if (timefield) { - attributes='class="eui-textNoWrap" width="1%"'; -} else if (sourcefield) { - attributes='class="eui-textBreakAll eui-textBreakWord"'; -} else { - attributes='class="kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord"'; -} -%> - data-test-subj="docTableField"> - <%= formatted %> - - <% if (filterable) { %> - - - <% } %> - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts new file mode 100644 index 000000000000000..c6d0d324b9bc20c --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { cell } from './cell'; + +describe('cell renderer', () => { + it('renders a cell without filter buttons if it is not filterable', () => { + expect( + cell({ + filterable: false, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: 'formatted content', + }) + ).toMatchInlineSnapshot(` + "formatted content + " + `); + }); + + it('renders a cell with filter buttons if it is filterable', () => { + expect( + cell({ + filterable: true, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: 'formatted content', + }) + ).toMatchInlineSnapshot(` + "formatted content + " + `); + }); + + it('renders a sourcefield', () => { + expect( + cell({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: true, + formatted: 'formatted content', + }) + ).toMatchInlineSnapshot(` + "formatted content + " + `); + }); + + it('renders a field that is neither a timefield or sourcefield', () => { + expect( + cell({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: false, + formatted: 'formatted content', + }) + ).toMatchInlineSnapshot(` + "formatted content + " + `); + }); + + it('renders the "formatted" contents without any manipulation', () => { + expect( + cell({ + filterable: false, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: + '
 hey you can put HTML & stuff in here 
', + }) + ).toMatchInlineSnapshot(` + "
 hey you can put HTML & stuff in here 
+ " + `); + }); + + it('escapes the contents of "column" within the "data-column" attribute', () => { + expect( + cell({ + filterable: true, + column: '', + timefield: true, + sourcefield: false, + formatted: 'formatted content', + }) + ).toMatchInlineSnapshot(` + "formatted content + " + `); + }); +}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts new file mode 100644 index 000000000000000..8138e0f4a4fd813 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { escape } from 'lodash'; +import cellWithFilters from './cell_with_buttons.html'; +import cellWithoutFilters from './cell_without_buttons.html'; + +const TAGS_WITH_WS = />\s+<'); +} + +const cellWithFiltersTemplate = noWhiteSpace(cellWithFilters); +const cellWithoutFiltersTemplate = noWhiteSpace(cellWithoutFilters); + +interface CellProps { + timefield: boolean; + sourcefield?: boolean; + formatted: string; + filterable: boolean; + column: string; +} + +export const cell = (props: CellProps) => { + let classes = ''; + let extraAttrs = ''; + if (props.timefield) { + classes = 'eui-textNoWrap'; + extraAttrs = 'width="1%"'; + } else if (props.sourcefield) { + classes = 'eui-textBreakAll eui-textBreakWord'; + } else { + classes = 'kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord'; + } + + if (props.filterable) { + const escapedColumnContents = escape(props.column); + return cellWithFiltersTemplate + .replace('__classes__', classes) + .replace('__extraAttrs__', extraAttrs) + .replace('__column__', escapedColumnContents) + .replace('__column__', escapedColumnContents) + .replace('', props.formatted); + } + return cellWithoutFiltersTemplate + .replace('__classes__', classes) + .replace('__extraAttrs__', extraAttrs) + .replace('', props.formatted); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html new file mode 100644 index 000000000000000..99c65e603401347 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html @@ -0,0 +1,25 @@ + + + + + + + diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html new file mode 100644 index 000000000000000..8dc33cbfb8353de --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.html deleted file mode 100644 index cf13f10e70060cf..000000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.html +++ /dev/null @@ -1,3 +0,0 @@ -
- <%= body %> -
\ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts new file mode 100644 index 000000000000000..70d8465589237cb --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { truncateByHeight } from './truncate_by_height'; + +describe('truncateByHeight', () => { + it('renders input without any formatting or escaping', () => { + expect( + truncateByHeight({ + body: + '
 hey you can put HTML & stuff in here 
', + }) + ).toMatchInlineSnapshot( + `"
 hey you can put HTML & stuff in here 
"` + ); + }); +}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts new file mode 100644 index 000000000000000..7eb31459eb4f514 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const truncateByHeight = ({ body }: { body: string }) => { + return `
${body}
`; +}; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx index 8037022085f0243..b2be40c0082006a 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx @@ -124,6 +124,33 @@ describe('DiscoverGrid', () => { expect(getDisplayedDocNr(component)).toBe(5); }); + test('showing selected documents, underlying data changes, all documents are displayed, selection is gone', async () => { + await toggleDocSelection(component, esHits[0]); + await toggleDocSelection(component, esHits[1]); + expect(getSelectedDocNr(component)).toBe(2); + findTestSubject(component, 'dscGridSelectionBtn').simulate('click'); + findTestSubject(component, 'dscGridShowSelectedDocuments').simulate('click'); + expect(getDisplayedDocNr(component)).toBe(2); + component.setProps({ + rows: [ + { + _index: 'i', + _id: '6', + _score: 1, + _type: '_doc', + _source: { + date: '2020-20-02T12:12:12.128', + name: 'test6', + extension: 'doc', + bytes: 50, + }, + }, + ], + }); + expect(getDisplayedDocNr(component)).toBe(1); + expect(getSelectedDocNr(component)).toBe(0); + }); + test('showing only selected documents and remove filter deselecting each doc manually', async () => { await toggleDocSelection(component, esHits[0]); findTestSubject(component, 'dscGridSelectionBtn').simulate('click'); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index be38f166fa1c0b8..f969eb32f3791ac 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -164,17 +164,33 @@ export const DiscoverGrid = ({ const [isFilterActive, setIsFilterActive] = useState(false); const displayedColumns = getDisplayedColumns(columns, indexPattern); const defaultColumns = displayedColumns.includes('_source'); + const usedSelectedDocs = useMemo(() => { + if (!selectedDocs.length || !rows?.length) { + return []; + } + const idMap = rows.reduce((map, row) => map.set(getDocId(row), true), new Map()); + // filter out selected docs that are no longer part of the current data + const result = selectedDocs.filter((docId) => idMap.get(docId)); + if (result.length === 0 && isFilterActive) { + setIsFilterActive(false); + } + return result; + }, [selectedDocs, rows, isFilterActive]); + const displayedRows = useMemo(() => { if (!rows) { return []; } - if (!isFilterActive || selectedDocs.length === 0) { + if (!isFilterActive || usedSelectedDocs.length === 0) { + return rows; + } + const rowsFiltered = rows.filter((row) => usedSelectedDocs.includes(getDocId(row))); + if (!rowsFiltered.length) { + // in case the selected docs are no longer part of the sample of 500, show all docs return rows; } - return rows.filter((row) => { - return selectedDocs.includes(getDocId(row)); - }); - }, [rows, selectedDocs, isFilterActive]); + return rowsFiltered; + }, [rows, usedSelectedDocs, isFilterActive]); /** * Pagination @@ -258,16 +274,16 @@ export const DiscoverGrid = ({ const additionalControls = useMemo( () => - selectedDocs.length ? ( + usedSelectedDocs.length ? ( ) : null, - [selectedDocs, isFilterActive, rows, setIsFilterActive] + [usedSelectedDocs, isFilterActive, rows, setIsFilterActive] ); if (!rowCount) { @@ -291,7 +307,7 @@ export const DiscoverGrid = ({ onFilter, indexPattern, isDarkMode: services.uiSettings.get('theme:darkMode'), - selectedDocs, + selectedDocs: usedSelectedDocs, setSelectedDocs: (newSelectedDocs) => { setSelectedDocs(newSelectedDocs); if (isFilterActive && newSelectedDocs.length === 0) { diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts index 9a12cb51eac0ca6..38bc831225c6824 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import moment from 'moment'; import { showOpenSearchPanel } from './show_open_search_panel'; import { getSharingData, showPublicUrlSwitch } from '../../helpers/get_sharing_data'; import { unhashUrl } from '../../../../../kibana_utils/public'; @@ -122,7 +123,13 @@ export const getTopNavLinks = ({ objectType: 'search', sharingData: { ...sharingData, - title: savedSearch.title, + // CSV reports can be generated without a saved search so we provide a fallback title + title: + savedSearch.title || + i18n.translate('discover.localMenu.fallbackReportTitle', { + defaultMessage: 'Discover search [{date}]', + values: { date: moment().toISOString(true) }, + }), }, isDirty: !savedSearch.id || state.isAppStateDirty(), showPublicUrlSwitch, diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 343ea9ef7f03c86..69687f75f309828 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -362,14 +362,21 @@ describe('Execution', () => { }); test('result is undefined until execution completes', async () => { + jest.useFakeTimers(); const execution = createExecution('sleep 10'); expect(execution.state.get().result).toBe(undefined); execution.start(null).subscribe(jest.fn()); expect(execution.state.get().result).toBe(undefined); - await new Promise((r) => setTimeout(r, 1)); + + jest.advanceTimersByTime(1); + await new Promise(process.nextTick); expect(execution.state.get().result).toBe(undefined); - await new Promise((r) => setTimeout(r, 11)); + + jest.advanceTimersByTime(10); + await new Promise(process.nextTick); expect(execution.state.get().result).toBe(null); + + jest.useRealTimers(); }); test('handles functions returning observables', () => { diff --git a/src/plugins/home/server/services/index.ts b/src/plugins/home/server/services/index.ts index 7f26c886ab4b644..5674a3501f0644f 100644 --- a/src/plugins/home/server/services/index.ts +++ b/src/plugins/home/server/services/index.ts @@ -15,7 +15,6 @@ export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials export { TutorialsCategory } from './tutorials'; export type { - ParamTypes, InstructionSetSchema, ParamsSchema, InstructionsSchema, diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts index 4d9dc3885e67dbb..09af7728f74d22a 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { SavedObject } from 'src/core/server'; +import type { SampleDatasetSchema } from './sample_dataset_schema'; +export type { SampleDatasetSchema, AppLinkSchema, DataIndexSchema } from './sample_dataset_schema'; export enum DatasetStatusTypes { NOT_INSTALLED = 'not_installed', @@ -26,57 +27,4 @@ export enum EmbeddableTypes { SEARCH_EMBEDDABLE_TYPE = 'search', VISUALIZE_EMBEDDABLE_TYPE = 'visualization', } -export interface DataIndexSchema { - id: string; - - // path to newline delimented JSON file containing data relative to KIBANA_HOME - dataPath: string; - - // Object defining Elasticsearch field mappings (contents of index.mappings.type.properties) - fields: object; - - // times fields that will be updated relative to now when data is installed - timeFields: string[]; - - // Reference to now in your test data set. - // When data is installed, timestamps are converted to the present time. - // The distance between a timestamp and currentTimeMarker is preserved but the date and time will change. - // For example: - // sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z - // installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z - currentTimeMarker: string; - - // Set to true to move timestamp to current week, preserving day of week and time of day - // Relative distance from timestamp to currentTimeMarker will not remain the same - preserveDayOfWeekTimeOfDay: boolean; -} - -export interface AppLinkSchema { - path: string; - icon: string; - label: string; -} - -export interface SampleDatasetSchema { - id: string; - name: string; - description: string; - previewImagePath: string; - darkPreviewImagePath: string; - - // saved object id of main dashboard for sample data set - overviewDashboard: string; - appLinks: AppLinkSchema[]; - - // saved object id of default index-pattern for sample data set - defaultIndex: string; - - // Kibana saved objects (index patter, visualizations, dashboard, ...) - // Should provide a nice demo of Kibana's functionality with the sample data set - savedObjects: Array>; - dataIndices: DataIndexSchema[]; - status?: string | undefined; - statusMsg?: unknown; -} - export type SampleDatasetProvider = () => SampleDatasetSchema; diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts index eb0b2252774b5fe..3c1764b2b8df1c1 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts @@ -5,22 +5,27 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { Writable } from '@kbn/utility-types'; +import { schema, TypeOf } from '@kbn/config-schema'; -import Joi from 'joi'; - -const dataIndexSchema = Joi.object({ - id: Joi.string() - .regex(/^[a-zA-Z0-9-]+$/) - .required(), +const idRegExp = /^[a-zA-Z0-9-]+$/; +const dataIndexSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!idRegExp.test(value)) { + return `Does not satisfy regexp: ${idRegExp.toString()}`; + } + }, + }), // path to newline delimented JSON file containing data relative to KIBANA_HOME - dataPath: Joi.string().required(), + dataPath: schema.string(), // Object defining Elasticsearch field mappings (contents of index.mappings.type.properties) - fields: Joi.object().required(), + fields: schema.recordOf(schema.string(), schema.any()), // times fields that will be updated relative to now when data is installed - timeFields: Joi.array().items(Joi.string()).required(), + timeFields: schema.arrayOf(schema.string()), // Reference to now in your test data set. // When data is installed, timestamps are converted to the present time. @@ -28,37 +33,66 @@ const dataIndexSchema = Joi.object({ // For example: // sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z // installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z - currentTimeMarker: Joi.string().isoDate().required(), + currentTimeMarker: schema.string({ + validate(value: string) { + if (isNaN(Date.parse(value))) { + return 'Expected a valid string in iso format'; + } + }, + }), // Set to true to move timestamp to current week, preserving day of week and time of day // Relative distance from timestamp to currentTimeMarker will not remain the same - preserveDayOfWeekTimeOfDay: Joi.boolean().default(false), + preserveDayOfWeekTimeOfDay: schema.boolean({ defaultValue: false }), }); -const appLinkSchema = Joi.object({ - path: Joi.string().required(), - label: Joi.string().required(), - icon: Joi.string().required(), +export type DataIndexSchema = TypeOf; + +const appLinkSchema = schema.object({ + path: schema.string(), + label: schema.string(), + icon: schema.string(), }); +export type AppLinkSchema = TypeOf; -export const sampleDataSchema = { - id: Joi.string() - .regex(/^[a-zA-Z0-9-]+$/) - .required(), - name: Joi.string().required(), - description: Joi.string().required(), - previewImagePath: Joi.string().required(), - darkPreviewImagePath: Joi.string(), +export const sampleDataSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!idRegExp.test(value)) { + return `Does not satisfy regexp: ${idRegExp.toString()}`; + } + }, + }), + name: schema.string(), + description: schema.string(), + previewImagePath: schema.string(), + darkPreviewImagePath: schema.maybe(schema.string()), // saved object id of main dashboard for sample data set - overviewDashboard: Joi.string().required(), - appLinks: Joi.array().items(appLinkSchema).default([]), + overviewDashboard: schema.string(), + appLinks: schema.arrayOf(appLinkSchema, { defaultValue: [] }), // saved object id of default index-pattern for sample data set - defaultIndex: Joi.string().required(), + defaultIndex: schema.string(), // Kibana saved objects (index patter, visualizations, dashboard, ...) // Should provide a nice demo of Kibana's functionality with the sample data set - savedObjects: Joi.array().items(Joi.object()).required(), - dataIndices: Joi.array().items(dataIndexSchema).required(), -}; + savedObjects: schema.arrayOf( + schema.object( + { + id: schema.string(), + type: schema.string(), + attributes: schema.any(), + references: schema.arrayOf(schema.any()), + version: schema.maybe(schema.any()), + }, + { unknowns: 'allow' } + ) + ), + dataIndices: schema.arrayOf(dataIndexSchema), + + status: schema.maybe(schema.string()), + statusMsg: schema.maybe(schema.string()), +}); + +export type SampleDatasetSchema = Writable>; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index e5ff33d5c199dd8..d0457f0a6d301c4 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -7,7 +7,12 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, Logger, IScopedClusterClient } from 'src/core/server'; +import type { + IRouter, + Logger, + IScopedClusterClient, + SavedObjectsBulkCreateObject, +} from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; import { @@ -148,8 +153,9 @@ export function createInstallRoute( const client = getClient({ includedHiddenTypes }); + const savedObjects = sampleDataset.savedObjects as SavedObjectsBulkCreateObject[]; createResults = await client.bulkCreate( - sampleDataset.savedObjects.map(({ version, ...savedObject }) => savedObject), + savedObjects.map(({ version, ...savedObject }) => savedObject), { overwrite: true } ); } catch (err) { diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.ts index ca75d20dc1d3f11..dff0d8640997480 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import Joi from 'joi'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { SavedObject } from 'src/core/public'; import { @@ -55,11 +54,13 @@ export class SampleDataRegistry { return { registerSampleDataset: (specProvider: SampleDatasetProvider) => { - const { error, value } = Joi.validate(specProvider(), sampleDataSchema); - - if (error) { + let value: SampleDatasetSchema; + try { + value = sampleDataSchema.validate(specProvider()); + } catch (error) { throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`); } + const defaultIndexSavedObjectJson = value.savedObjects.find((savedObjectJson: any) => { return ( savedObjectJson.type === 'index-pattern' && savedObjectJson.id === value.defaultIndex diff --git a/src/plugins/home/server/services/tutorials/index.ts b/src/plugins/home/server/services/tutorials/index.ts index 92f6de716185d58..f745d0190efd57f 100644 --- a/src/plugins/home/server/services/tutorials/index.ts +++ b/src/plugins/home/server/services/tutorials/index.ts @@ -12,7 +12,6 @@ export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials export { TutorialsCategory } from './lib/tutorials_registry_types'; export type { - ParamTypes, InstructionSetSchema, ParamsSchema, InstructionsSchema, diff --git a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts index 0f06b6c3257c259..5efbe067f6ece10 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts @@ -5,121 +5,153 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { schema, TypeOf } from '@kbn/config-schema'; -import Joi from 'joi'; - -const PARAM_TYPES = { - NUMBER: 'number', - STRING: 'string', -}; - -const TUTORIAL_CATEGORY = { - LOGGING: 'logging', - SECURITY_SOLUTION: 'security solution', - METRICS: 'metrics', - OTHER: 'other', -}; - -const dashboardSchema = Joi.object({ - id: Joi.string().required(), // Dashboard saved object id - linkLabel: Joi.string().when('isOverview', { - is: true, - then: Joi.required(), - }), +const dashboardSchema = schema.object({ + // Dashboard saved object id + id: schema.string(), // Is this an Overview / Entry Point dashboard? - isOverview: Joi.boolean().required(), + isOverview: schema.boolean(), + linkLabel: schema.conditional( + schema.siblingRef('isOverview'), + true, + schema.string(), + schema.maybe(schema.string()) + ), }); +export type DashboardSchema = TypeOf; -const artifactsSchema = Joi.object({ +const artifactsSchema = schema.object({ // Fields present in Elasticsearch documents created by this product. - exportedFields: Joi.object({ - documentationUrl: Joi.string().required(), - }), + exportedFields: schema.maybe( + schema.object({ + documentationUrl: schema.string(), + }) + ), // Kibana dashboards created by this product. - dashboards: Joi.array().items(dashboardSchema).required(), - application: Joi.object({ - path: Joi.string().required(), - label: Joi.string().required(), - }), + dashboards: schema.arrayOf(dashboardSchema), + application: schema.maybe( + schema.object({ + path: schema.string(), + label: schema.string(), + }) + ), }); - -const statusCheckSchema = Joi.object({ - title: Joi.string(), - text: Joi.string(), - btnLabel: Joi.string(), - success: Joi.string(), - error: Joi.string(), - esHitsCheck: Joi.object({ - index: Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string())).required(), - query: Joi.object().required(), - }).required(), +export type ArtifactsSchema = TypeOf; + +const statusCheckSchema = schema.object({ + title: schema.maybe(schema.string()), + text: schema.maybe(schema.string()), + btnLabel: schema.maybe(schema.string()), + success: schema.maybe(schema.string()), + error: schema.maybe(schema.string()), + esHitsCheck: schema.object({ + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + query: schema.recordOf(schema.string(), schema.any()), + }), }); -const instructionSchema = Joi.object({ - title: Joi.string(), - textPre: Joi.string(), - commands: Joi.array().items(Joi.string().allow('')), - textPost: Joi.string(), +const instructionSchema = schema.object({ + title: schema.maybe(schema.string()), + textPre: schema.maybe(schema.string()), + commands: schema.maybe(schema.arrayOf(schema.string())), + textPost: schema.maybe(schema.string()), }); +export type Instruction = TypeOf; -const instructionVariantSchema = Joi.object({ - id: Joi.string().required(), - instructions: Joi.array().items(instructionSchema).required(), +const instructionVariantSchema = schema.object({ + id: schema.string(), + instructions: schema.arrayOf(instructionSchema), }); -const instructionSetSchema = Joi.object({ - title: Joi.string(), - callOut: Joi.object({ - title: Joi.string().required(), - message: Joi.string(), - iconType: Joi.string(), - }), +export type InstructionVariant = TypeOf; + +const instructionSetSchema = schema.object({ + title: schema.maybe(schema.string()), + callOut: schema.maybe( + schema.object({ + title: schema.string(), + message: schema.maybe(schema.string()), + iconType: schema.maybe(schema.string()), + }) + ), // Variants (OSes, languages, etc.) for which tutorial instructions are specified. - instructionVariants: Joi.array().items(instructionVariantSchema).required(), - statusCheck: statusCheckSchema, + instructionVariants: schema.arrayOf(instructionVariantSchema), + statusCheck: schema.maybe(statusCheckSchema), }); - -const paramSchema = Joi.object({ - defaultValue: Joi.required(), - id: Joi.string() - .regex(/^[a-zA-Z_]+$/) - .required(), - label: Joi.string().required(), - type: Joi.string().valid(Object.values(PARAM_TYPES)).required(), +export type InstructionSetSchema = TypeOf; + +const idRegExp = /^[a-zA-Z_]+$/; +const paramSchema = schema.object({ + defaultValue: schema.any(), + id: schema.string({ + validate(value: string) { + if (!idRegExp.test(value)) { + return `Does not satisfy regexp ${idRegExp.toString()}`; + } + }, + }), + label: schema.string(), + type: schema.oneOf([schema.literal('number'), schema.literal('string')]), }); +export type ParamsSchema = TypeOf; -const instructionsSchema = Joi.object({ - instructionSets: Joi.array().items(instructionSetSchema).required(), - params: Joi.array().items(paramSchema), +const instructionsSchema = schema.object({ + instructionSets: schema.arrayOf(instructionSetSchema), + params: schema.maybe(schema.arrayOf(paramSchema)), }); - -export const tutorialSchema = { - id: Joi.string() - .regex(/^[a-zA-Z0-9-]+$/) - .required(), - category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(), - name: Joi.string().required(), - moduleName: Joi.string(), - isBeta: Joi.boolean().default(false), - shortDescription: Joi.string().required(), - euiIconType: Joi.string(), // EUI icon type string, one of https://elastic.github.io/eui/#/icons - longDescription: Joi.string().required(), - completionTimeMinutes: Joi.number().integer(), - previewImagePath: Joi.string(), - +export type InstructionsSchema = TypeOf; + +const tutorialIdRegExp = /^[a-zA-Z0-9-]+$/; +export const tutorialSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!tutorialIdRegExp.test(value)) { + return `Does not satisfy regexp ${tutorialIdRegExp.toString()}`; + } + }, + }), + category: schema.oneOf([ + schema.literal('logging'), + schema.literal('security'), + schema.literal('metrics'), + schema.literal('other'), + ]), + name: schema.string({ + validate(value: string) { + if (value === '') { + return 'is not allowed to be empty'; + } + }, + }), + moduleName: schema.maybe(schema.string()), + isBeta: schema.maybe(schema.boolean()), + shortDescription: schema.string(), + // EUI icon type string, one of https://elastic.github.io/eui/#/icons + euiIconType: schema.maybe(schema.string()), + longDescription: schema.string(), + completionTimeMinutes: schema.maybe( + schema.number({ + validate(value: number) { + if (!Number.isInteger(value)) { + return 'Expected to be a valid integer number'; + } + }, + }) + ), + previewImagePath: schema.maybe(schema.string()), // kibana and elastic cluster running on prem - onPrem: instructionsSchema.required(), - + onPrem: instructionsSchema, // kibana and elastic cluster running in elastic's cloud - elasticCloud: instructionsSchema, - + elasticCloud: schema.maybe(instructionsSchema), // kibana running on prem and elastic cluster running in elastic's cloud - onPremElasticCloud: instructionsSchema, - + onPremElasticCloud: schema.maybe(instructionsSchema), // Elastic stack artifacts produced by product when it is setup and run. - artifacts: artifactsSchema, + artifacts: schema.maybe(artifactsSchema), // saved objects used by data module. - savedObjects: Joi.array().items(), - savedObjectsInstallMsg: Joi.string(), -}; + savedObjects: schema.maybe(schema.arrayOf(schema.any())), + savedObjectsInstallMsg: schema.maybe(schema.string()), +}); + +export type TutorialSchema = TypeOf; diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index b0837a99d65adab..4c80c8858a4753b 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -6,8 +6,18 @@ * Side Public License, v 1. */ -import { IconType } from '@elastic/eui'; -import { KibanaRequest } from 'src/core/server'; +import type { KibanaRequest } from 'src/core/server'; +import type { TutorialSchema } from './tutorial_schema'; +export type { + TutorialSchema, + ArtifactsSchema, + DashboardSchema, + InstructionsSchema, + ParamsSchema, + InstructionSetSchema, + InstructionVariant, + Instruction, +} from './tutorial_schema'; /** @public */ export enum TutorialsCategory { @@ -18,82 +28,6 @@ export enum TutorialsCategory { } export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM'; -export interface ParamTypes { - NUMBER: string; - STRING: string; -} -export interface Instruction { - title?: string; - textPre?: string; - commands?: string[]; - textPost?: string; -} -export interface InstructionVariant { - id: string; - instructions: Instruction[]; -} -export interface InstructionSetSchema { - readonly title?: string; - readonly callOut?: { - title: string; - message?: string; - iconType?: IconType; - }; - instructionVariants: InstructionVariant[]; -} -export interface ParamsSchema { - defaultValue: any; - id: string; - label: string; - type: ParamTypes; -} -export interface InstructionsSchema { - readonly instructionSets: InstructionSetSchema[]; - readonly params?: ParamsSchema[]; -} -export interface DashboardSchema { - id: string; - linkLabel?: string; - isOverview: boolean; -} -export interface ArtifactsSchema { - exportedFields?: { - documentationUrl: string; - }; - dashboards: DashboardSchema[]; - application?: { - path: string; - label: string; - }; -} -export interface TutorialSchema { - id: string; - category: TutorialsCategory; - name: string; - moduleName?: string; - isBeta?: boolean; - shortDescription: string; - euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/display/icons; - longDescription: string; - completionTimeMinutes?: number; - previewImagePath?: string; - - // kibana and elastic cluster running on prem - onPrem: InstructionsSchema; - - // kibana and elastic cluster running in elastic's cloud - elasticCloud?: InstructionsSchema; - - // kibana running on prem and elastic cluster running in elastic's cloud - onPremElasticCloud?: InstructionsSchema; - - // Elastic stack artifacts produced by product when it is setup and run. - artifacts?: ArtifactsSchema; - - // saved objects used by data module. - savedObjects?: any[]; - savedObjectsInstallMsg?: string; -} export interface TutorialContext { [key: string]: unknown; } diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index 94f5d656100837e..a82699c231ad467 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -92,7 +92,7 @@ describe('TutorialsRegistry', () => { const setup = new TutorialsRegistry().setup(mockCoreSetup); testProvider = ({}) => invalidTutorialProvider; expect(() => setup.registerTutorial(testProvider)).toThrowErrorMatchingInlineSnapshot( - `"Unable to register tutorial spec because its invalid. ValidationError: child \\"name\\" fails because [\\"name\\" is not allowed to be empty]"` + `"Unable to register tutorial spec because its invalid. Error: [name]: is not allowed to be empty"` ); }); diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.ts index f21f2ccd719c5e3..05f5600af307af5 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import Joi from 'joi'; import { CoreSetup } from 'src/core/server'; import { TutorialProvider, @@ -42,10 +41,10 @@ export class TutorialsRegistry { ); return { registerTutorial: (specProvider: TutorialProvider) => { - const emptyContext = {}; - const { error } = Joi.validate(specProvider(emptyContext), tutorialSchema); - - if (error) { + try { + const emptyContext = {}; + tutorialSchema.validate(specProvider(emptyContext)); + } catch (error) { throw new Error(`Unable to register tutorial spec because its invalid. ${error}`); } diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts index 9577f25184ba0af..70f180d7cb5f281 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -11,11 +11,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}`; + const kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`; return { - runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + runtimePainless: `${kibanaDocsBase}/managing-index-patterns.html#runtime-fields`, painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, }; }; diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 1402416d69c9677..90c9c2888c9da8b 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { AddConfigDeprecation, CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; +import type { CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; import { get } from 'lodash'; import { configSchema, ConfigSchema } from '../config'; @@ -19,16 +19,12 @@ export const config: PluginConfigDescriptor = { deprecations: ({ renameFromRoot }) => [ // TODO: Remove deprecation once defaultAppId is deleted renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', { silent: true }), - ( - completeConfig: Record, - rootPath: string, - addDeprecation: AddConfigDeprecation - ) => { + (completeConfig, rootPath, addDeprecation) => { if ( get(completeConfig, 'kibana.defaultAppId') === undefined && get(completeConfig, 'kibana_legacy.defaultAppId') === undefined ) { - return completeConfig; + return; } addDeprecation({ message: `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead`, @@ -40,7 +36,6 @@ export const config: PluginConfigDescriptor = { ], }, }); - return completeConfig; }, ], }; diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 51344e2d28ab67e..55e10e7861e518d 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -34,14 +34,14 @@ export interface Props { value: string; /** Function invoked when text in editor is changed */ - onChange: (value: string) => void; + onChange: (value: string, event: monaco.editor.IModelContentChangedEvent) => void; /** * Options for the Monaco Code Editor * Documentation of options can be found here: - * https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html + * https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html */ - options?: monaco.editor.IEditorConstructionOptions; + options?: monaco.editor.IStandaloneEditorConstructionOptions; /** * Suggestion provider for autocompletion diff --git a/src/plugins/kibana_react/public/code_editor/index.tsx b/src/plugins/kibana_react/public/code_editor/index.tsx index 635e84b1d8c2023..2440974c3b1d1ea 100644 --- a/src/plugins/kibana_react/public/code_editor/index.tsx +++ b/src/plugins/kibana_react/public/code_editor/index.tsx @@ -16,7 +16,7 @@ import { import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { useUiSetting } from '../ui_settings'; -import type { Props } from './code_editor'; +import { Props } from './code_editor'; const LazyBaseEditor = React.lazy(() => import('./code_editor')); @@ -26,6 +26,8 @@ const Fallback = () => ( ); +export type CodeEditorProps = Props; + /** * Renders a Monaco code editor with EUI color theme. * diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 06d1cd290ffd53d..f1592d5a8cf0b1e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -444,4 +444,12 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'labs:dashboard:enable_ui': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, + 'labs:dashboard:deferBelowFold': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index dfbe6bd3e04854b..570b52171be283f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -121,4 +121,6 @@ export interface UsageStats { 'labs:canvas:enable_ui': boolean; 'labs:canvas:useDataService': boolean; 'labs:presentation:timeToPresent': boolean; + 'labs:dashboard:enable_ui': boolean; + 'labs:dashboard:deferBelowFold': boolean; } diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index d551b733ecb8a66..7ca5272daa9c7e5 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -8,10 +8,12 @@ import { i18n } from '@kbn/i18n'; -export const USE_DATA_SERVICE = 'labs:canvas:useDataService'; -export const TIME_TO_PRESENT = 'labs:presentation:timeToPresent'; +export const LABS_PROJECT_PREFIX = 'labs:'; +export const USE_DATA_SERVICE = `${LABS_PROJECT_PREFIX}canvas:useDataService` as const; +export const TIME_TO_PRESENT = `${LABS_PROJECT_PREFIX}presentation:timeToPresent` as const; +export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; -export const projectIDs = [TIME_TO_PRESENT, USE_DATA_SERVICE] as const; +export const projectIDs = [TIME_TO_PRESENT, USE_DATA_SERVICE, DEFER_BELOW_FOLD] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -49,6 +51,20 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { ), solutions: ['canvas'], }, + [DEFER_BELOW_FOLD]: { + id: DEFER_BELOW_FOLD, + isActive: false, + isDisplayed: true, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableDeferBelowFoldProjectName', { + defaultMessage: 'Defer loading below "the fold"', + }), + description: i18n.translate('presentationUtil.labs.enableDeferBelowFoldProjectDescription', { + defaultMessage: + 'Any Dashboard panels below the fold-- the area hidden beyond the bottom of the window, accessed by scrolling-- will not be loaded immediately, but only when they enter the viewport', + }), + solutions: ['dashboard'], + }, }; export type ProjectID = typeof projectIDs[number]; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index aee3cff92438bc4..5ad81c7e759bce1 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -12,6 +12,7 @@ export { PresentationCapabilitiesService, PresentationDashboardsService, PresentationLabsService, + getStubPluginServices, } from './services'; export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; @@ -40,3 +41,7 @@ export { export function plugin() { return new PresentationUtilPlugin(); } + +import { pluginServices } from './services'; + +export const useLabs = () => (() => pluginServices.getHooks().labs.useService())(); diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 30bab78aeb27b49..d68779b129ca697 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { PresentationUtilPluginStart } from '../types'; import { PluginServices } from './create'; import { PresentationCapabilitiesService } from './capabilities'; import { PresentationDashboardsService } from './dashboards'; import { PresentationLabsService } from './labs'; +import { registry as stubRegistry } from './stub'; export { PresentationCapabilitiesService } from './capabilities'; export { PresentationDashboardsService } from './dashboards'; @@ -21,3 +23,11 @@ export interface PresentationUtilServices { } export const pluginServices = new PluginServices(); + +export const getStubPluginServices = (): PresentationUtilPluginStart => { + pluginServices.setRegistry(stubRegistry.start({})); + return { + ContextProvider: pluginServices.getContextProvider(), + labsService: pluginServices.getServices().labs, + }; +}; diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts index db781034698806a..fe0767ff09d8f45 100644 --- a/src/plugins/presentation_util/public/services/kibana/labs.ts +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -7,7 +7,6 @@ */ import { - environmentNames, EnvironmentName, projectIDs, projects, @@ -15,6 +14,7 @@ import { Project, getProjectIDs, SolutionName, + LABS_PROJECT_PREFIX, } from '../../../common'; import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; @@ -31,6 +31,16 @@ export type LabsServiceFactory = KibanaPluginServiceFactory< PresentationUtilPluginStartDeps >; +const clearLabsFromStorage = (storage: Storage) => { + projectIDs.forEach((projectID) => storage.removeItem(projectID)); + + // This is a redundancy, to catch any labs that may have been removed above. + // We could consider gathering telemetry to see how often this happens, or this may be unnecessary. + Object.keys(storage) + .filter((key) => key.startsWith(LABS_PROJECT_PREFIX)) + .forEach((key) => storage.removeItem(key)); +}; + export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { const { uiSettings } = coreStart; const localStorage = window.localStorage; @@ -75,17 +85,18 @@ export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { }; const reset = () => { - localStorage.clear(); - sessionStorage.clear(); - environmentNames.forEach((env) => - projectIDs.forEach((id) => setProjectStatus(id, env, projects[id].isActive)) - ); + clearLabsFromStorage(localStorage); + clearLabsFromStorage(sessionStorage); + projectIDs.forEach((id) => setProjectStatus(id, 'kibana', projects[id].isActive)); }; + const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; + return { getProjectIDs, getProjects, getProject, + isProjectEnabled, reset, setProjectStatus, }; diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts index ef583bd4189a94d..70c40eaafa2ef2a 100644 --- a/src/plugins/presentation_util/public/services/labs.ts +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -20,6 +20,7 @@ import { } from '../../common'; export interface PresentationLabsService { + isProjectEnabled: (id: ProjectID) => boolean; getProjectIDs: () => typeof projectIDs; getProject: (id: ProjectID) => Project; getProjects: (solutions?: SolutionName[]) => Record; diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts index 396db5246005336..8bc526987d95fd0 100644 --- a/src/plugins/presentation_util/public/services/storybook/labs.ts +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -46,13 +46,17 @@ export const labsServiceFactory: LabsServiceFactory = () => { }; const reset = () => { + // This is normally not ok, but it's our isolated Storybook instance. storage.clear(); }; + const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; + return { getProjectIDs, getProjects, getProject, + isProjectEnabled, reset, setProjectStatus, }; diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts index c511ed26ef32e44..5192f5f090fec7b 100644 --- a/src/plugins/presentation_util/public/services/stub/labs.ts +++ b/src/plugins/presentation_util/public/services/stub/labs.ts @@ -20,10 +20,33 @@ import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } export type LabsServiceFactory = PluginServiceFactory; +type Statuses = { + [id in ProjectID]: { + defaultValue: boolean; + session: boolean | null; + browser: boolean | null; + kibana: boolean; + }; +}; + export const labsServiceFactory: LabsServiceFactory = () => { + let statuses = {} as Statuses; + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const value = statuses[id]; + const status = { + session: isEnabledByStorageValue(project, 'session', value.session), + browser: isEnabledByStorageValue(project, 'browser', value.browser), + kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), + }; + + return applyProjectStatus(project, status); + }; + const reset = () => projectIDs.reduce((acc, id) => { - const project = getProject(id); + const project = projects[id]; const defaultValue = project.isActive; acc[id] = { @@ -33,9 +56,9 @@ export const labsServiceFactory: LabsServiceFactory = () => { kibana: defaultValue, }; return acc; - }, {} as { [id in ProjectID]: { defaultValue: boolean; session: boolean | null; browser: boolean | null; kibana: boolean } }); + }, {} as Statuses); - let statuses = reset(); + statuses = reset(); const getProjects = (solutions: SolutionName[] = []) => projectIDs.reduce((acc, id) => { @@ -49,26 +72,17 @@ export const labsServiceFactory: LabsServiceFactory = () => { return acc; }, {} as { [id in ProjectID]: Project }); - const getProject = (id: ProjectID) => { - const project = projects[id]; - const value = statuses[id]; - const status = { - session: isEnabledByStorageValue(project, 'session', value.session), - browser: isEnabledByStorageValue(project, 'browser', value.browser), - kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), - }; - - return applyProjectStatus(project, status); - }; - const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { statuses[id] = { ...statuses[id], [env]: value }; }; + const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; + return { getProjectIDs, getProject, getProjects, + isProjectEnabled, setProjectStatus, reset: () => { statuses = reset(); diff --git a/src/plugins/screenshot_mode/.i18nrc.json b/src/plugins/screenshot_mode/.i18nrc.json new file mode 100644 index 000000000000000..79643fbb63d30d6 --- /dev/null +++ b/src/plugins/screenshot_mode/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "screenshotMode", + "paths": { + "screenshotMode": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/src/plugins/screenshot_mode/README.md b/src/plugins/screenshot_mode/README.md new file mode 100755 index 000000000000000..faa298b33d5fa22 --- /dev/null +++ b/src/plugins/screenshot_mode/README.md @@ -0,0 +1,27 @@ +# Screenshot Mode + +The service exposed by this plugin informs consumers whether they should optimize for non-interactivity. In this way plugins can avoid loading unnecessary code, data or other services. + +The primary intention is to inform other lower-level plugins (plugins that don't depend on other plugins) that we do not expect an actual user to interact with browser. In this way we can avoid loading unnecessary resources (code and data). + +**NB** This plugin should have no other dependencies to avoid any possibility of circular dependencies. + +--- + +## Development + +### How to test in screenshot mode + +Please note: the following information is subject to change over time. + +In order to test whether we are correctly detecting screenshot mode, developers can run the following JS snippet: + +```js +window.localStorage.setItem('__KBN_SCREENSHOT_MODE_ENABLED_KEY__', true); +``` + +To get out of screenshot mode, run the following snippet: + +```js +window.localStorage.removeItem('__KBN_SCREENSHOT_MODE_ENABLED_KEY__'); +``` diff --git a/src/plugins/screenshot_mode/common/constants.ts b/src/plugins/screenshot_mode/common/constants.ts new file mode 100644 index 000000000000000..d5073f5920c0e69 --- /dev/null +++ b/src/plugins/screenshot_mode/common/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const KBN_SCREENSHOT_MODE_HEADER = 'x-kbn-screenshot-mode'.toLowerCase(); diff --git a/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts b/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts new file mode 100644 index 000000000000000..7714f88cebeec6c --- /dev/null +++ b/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// **PLEASE NOTE** +// The functionality in this file targets a browser environment and is intended to be used both in public and server. +// For instance, reporting uses these functions when starting puppeteer to set the current browser into "screenshot" mode. + +export const KBN_SCREENSHOT_MODE_ENABLED_KEY = '__KBN_SCREENSHOT_MODE_ENABLED_KEY__'; + +/** + * This function is responsible for detecting whether we are currently in screenshot mode. + * + * We check in the current window context whether screenshot mode is enabled, otherwise we check + * localStorage. The ability to set a value in localStorage enables more convenient development and testing + * in functionality that needs to detect screenshot mode. + */ +export const getScreenshotMode = (): boolean => { + return ( + ((window as unknown) as Record)[KBN_SCREENSHOT_MODE_ENABLED_KEY] === true || + window.localStorage.getItem(KBN_SCREENSHOT_MODE_ENABLED_KEY) === 'true' + ); +}; + +/** + * Use this function to set the current browser to screenshot mode. + * + * This function should be called as early as possible to ensure that screenshot mode is + * correctly detected for the first page load. It is not suitable for use inside any plugin + * code unless the plugin code is guaranteed to, somehow, load before any other code. + * + * Additionally, we don't know what environment this code will run in so we remove as many external + * references as possible to make it portable. For instance, running inside puppeteer. + */ +export const setScreenshotModeEnabled = () => { + Object.defineProperty( + window, + '__KBN_SCREENSHOT_MODE_ENABLED_KEY__', // Literal value to prevent adding an external reference + { + enumerable: true, + writable: true, + configurable: false, + value: true, + } + ); +}; + +export const setScreenshotModeDisabled = () => { + Object.defineProperty( + window, + '__KBN_SCREENSHOT_MODE_ENABLED_KEY__', // Literal value to prevent adding an external reference + { + enumerable: true, + writable: true, + configurable: false, + value: undefined, + } + ); +}; diff --git a/src/plugins/screenshot_mode/common/index.ts b/src/plugins/screenshot_mode/common/index.ts new file mode 100644 index 000000000000000..fd9ad6f70febac1 --- /dev/null +++ b/src/plugins/screenshot_mode/common/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + getScreenshotMode, + setScreenshotModeEnabled, + setScreenshotModeDisabled, +} from './get_set_browser_screenshot_mode'; + +export { KBN_SCREENSHOT_MODE_HEADER } from './constants'; diff --git a/src/plugins/screenshot_mode/jest.config.js b/src/plugins/screenshot_mode/jest.config.js new file mode 100644 index 000000000000000..e84f3742f8c1d97 --- /dev/null +++ b/src/plugins/screenshot_mode/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/screenshot_mode'], +}; diff --git a/src/plugins/screenshot_mode/kibana.json b/src/plugins/screenshot_mode/kibana.json new file mode 100644 index 000000000000000..67c40b20be525a9 --- /dev/null +++ b/src/plugins/screenshot_mode/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "screenshotMode", + "version": "1.0.0", + "kibanaVersion": "kibana", + "ui": true, + "server": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts new file mode 100644 index 000000000000000..6a46b240d592e75 --- /dev/null +++ b/src/plugins/screenshot_mode/public/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ScreenshotModePlugin } from './plugin'; + +export function plugin() { + return new ScreenshotModePlugin(); +} + +export { KBN_SCREENSHOT_MODE_HEADER, setScreenshotModeEnabled } from '../common'; + +export { ScreenshotModePluginSetup } from './types'; diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts new file mode 100644 index 000000000000000..33ae5014668760f --- /dev/null +++ b/src/plugins/screenshot_mode/public/plugin.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { coreMock } from '../../../../src/core/public/mocks'; +import { ScreenshotModePlugin } from './plugin'; +import { setScreenshotModeEnabled, setScreenshotModeDisabled } from '../common'; + +describe('Screenshot mode public', () => { + let plugin: ScreenshotModePlugin; + + beforeEach(() => { + plugin = new ScreenshotModePlugin(); + }); + + afterAll(() => { + setScreenshotModeDisabled(); + }); + + describe('setup contract', () => { + it('detects screenshot mode "true"', () => { + setScreenshotModeEnabled(); + const screenshotMode = plugin.setup(coreMock.createSetup()); + expect(screenshotMode.isScreenshotMode()).toBe(true); + }); + + it('detects screenshot mode "false"', () => { + setScreenshotModeDisabled(); + const screenshotMode = plugin.setup(coreMock.createSetup()); + expect(screenshotMode.isScreenshotMode()).toBe(false); + }); + }); + + describe('start contract', () => { + it('returns nothing', () => { + expect(plugin.start(coreMock.createStart())).toBe(undefined); + }); + }); +}); diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts new file mode 100644 index 000000000000000..7a166566a0173bc --- /dev/null +++ b/src/plugins/screenshot_mode/public/plugin.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; + +import { ScreenshotModePluginSetup } from './types'; + +import { getScreenshotMode } from '../common'; + +export class ScreenshotModePlugin implements Plugin { + public setup(core: CoreSetup): ScreenshotModePluginSetup { + return { + isScreenshotMode: () => getScreenshotMode() === true, + }; + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts new file mode 100644 index 000000000000000..744ea8615f2a790 --- /dev/null +++ b/src/plugins/screenshot_mode/public/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface IScreenshotModeService { + /** + * Returns a boolean indicating whether the current user agent (browser) would like to view UI optimized for + * screenshots or printing. + */ + isScreenshotMode: () => boolean; +} + +export type ScreenshotModePluginSetup = IScreenshotModeService; diff --git a/src/plugins/screenshot_mode/server/index.ts b/src/plugins/screenshot_mode/server/index.ts new file mode 100644 index 000000000000000..68714e9a21b8743 --- /dev/null +++ b/src/plugins/screenshot_mode/server/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ScreenshotModePlugin } from './plugin'; + +export { setScreenshotModeEnabled, KBN_SCREENSHOT_MODE_HEADER } from '../common'; + +export { + ScreenshotModeRequestHandlerContext, + ScreenshotModePluginSetup, + ScreenshotModePluginStart, +} from './types'; + +export function plugin() { + return new ScreenshotModePlugin(); +} diff --git a/src/plugins/screenshot_mode/server/is_screenshot_mode.test.ts b/src/plugins/screenshot_mode/server/is_screenshot_mode.test.ts new file mode 100644 index 000000000000000..6d783970bd36286 --- /dev/null +++ b/src/plugins/screenshot_mode/server/is_screenshot_mode.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { httpServerMock } from 'src/core/server/mocks'; +import { KBN_SCREENSHOT_MODE_HEADER } from '../common'; +import { isScreenshotMode } from './is_screenshot_mode'; + +const { createKibanaRequest } = httpServerMock; + +describe('isScreenshotMode', () => { + test('screenshot headers are present', () => { + expect( + isScreenshotMode(createKibanaRequest({ headers: { [KBN_SCREENSHOT_MODE_HEADER]: 'true' } })) + ).toBe(true); + }); + + test('screenshot headers are not present', () => { + expect(isScreenshotMode(createKibanaRequest())).toBe(false); + }); +}); diff --git a/src/plugins/screenshot_mode/server/is_screenshot_mode.ts b/src/plugins/screenshot_mode/server/is_screenshot_mode.ts new file mode 100644 index 000000000000000..79787bcd1fb503d --- /dev/null +++ b/src/plugins/screenshot_mode/server/is_screenshot_mode.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaRequest } from 'src/core/server'; +import { KBN_SCREENSHOT_MODE_HEADER } from '../common'; + +export const isScreenshotMode = (request: KibanaRequest): boolean => { + return Object.keys(request.headers).some((header) => { + return header.toLowerCase() === KBN_SCREENSHOT_MODE_HEADER; + }); +}; diff --git a/src/plugins/screenshot_mode/server/plugin.ts b/src/plugins/screenshot_mode/server/plugin.ts new file mode 100644 index 000000000000000..9ef410d999ea508 --- /dev/null +++ b/src/plugins/screenshot_mode/server/plugin.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup } from '../../../core/server'; +import { + ScreenshotModeRequestHandlerContext, + ScreenshotModePluginSetup, + ScreenshotModePluginStart, +} from './types'; +import { isScreenshotMode } from './is_screenshot_mode'; + +export class ScreenshotModePlugin + implements Plugin { + public setup(core: CoreSetup): ScreenshotModePluginSetup { + core.http.registerRouteHandlerContext( + 'screenshotMode', + (ctx, req) => { + return { + isScreenshot: isScreenshotMode(req), + }; + } + ); + + // We use "require" here to ensure the import does not have external references due to code bundling that + // commonly happens during transpiling. External references would be missing in the environment puppeteer creates. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { setScreenshotModeEnabled } = require('../common'); + + return { + setScreenshotModeEnabled, + isScreenshotMode, + }; + } + + public start(): ScreenshotModePluginStart { + return { + isScreenshotMode, + }; + } + + public stop() {} +} diff --git a/src/plugins/screenshot_mode/server/types.ts b/src/plugins/screenshot_mode/server/types.ts new file mode 100644 index 000000000000000..4347252e58fce71 --- /dev/null +++ b/src/plugins/screenshot_mode/server/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; + +/** + * Any context that requires access to the screenshot mode flag but does not have access + * to request context {@link ScreenshotModeRequestHandlerContext}, for instance if they are pre-context, + * can use this function to check whether the request originates from a client that is in screenshot mode. + */ +type IsScreenshotMode = (request: KibanaRequest) => boolean; + +export interface ScreenshotModePluginSetup { + isScreenshotMode: IsScreenshotMode; + + /** + * Set the current environment to screenshot mode. Intended to run in a browser-environment. + */ + setScreenshotModeEnabled: () => void; +} + +export interface ScreenshotModePluginStart { + isScreenshotMode: IsScreenshotMode; +} + +export interface ScreenshotModeRequestHandlerContext extends RequestHandlerContext { + screenshotMode: { + isScreenshot: boolean; + }; +} diff --git a/src/plugins/screenshot_mode/tsconfig.json b/src/plugins/screenshot_mode/tsconfig.json new file mode 100644 index 000000000000000..58194b385448ba8 --- /dev/null +++ b/src/plugins/screenshot_mode/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + ] +} diff --git a/src/plugins/spaces_oss/public/api.mock.ts b/src/plugins/spaces_oss/public/api.mock.ts index 84b22459a96e223..9ad7599b5ae610a 100644 --- a/src/plugins/spaces_oss/public/api.mock.ts +++ b/src/plugins/spaces_oss/public/api.mock.ts @@ -11,7 +11,7 @@ import { of } from 'rxjs'; import type { SpacesApi, SpacesApiUi, SpacesApiUiComponent } from './api'; const createApiMock = (): jest.Mocked => ({ - activeSpace$: of(), + getActiveSpace$: jest.fn().mockReturnValue(of()), getActiveSpace: jest.fn(), ui: createApiUiMock(), }); diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index bd452c2fca00eca..ddee9c0528ba1af 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -19,7 +19,7 @@ export interface SpacesApi { * Observable representing the currently active space. * The details of the space can change without a full page reload (such as display name, color, etc.) */ - readonly activeSpace$: Observable; + getActiveSpace$(): Observable; /** * Retrieve the currently active space. @@ -169,8 +169,8 @@ export interface ShareToSpaceFlyoutProps { behaviorContext?: 'within-space' | 'outside-space'; /** * Optional handler that is called when the user has saved changes and there are spaces to be added to and/or removed from the object. If - * this is not defined, a default handler will be used that calls `/api/spaces/_share_saved_object_add` and/or - * `/api/spaces/_share_saved_object_remove` and displays toast(s) indicating what occurred. + * this is not defined, a default handler will be used that calls `/api/spaces/_update_objects_spaces` and displays a toast indicating + * what occurred. */ changeSpacesHandler?: (spacesToAdd: string[], spacesToRemove: string[]) => Promise; /** diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 30706fb2bfc3c26..230d2052f089ed5 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8278,6 +8278,18 @@ "_meta": { "description": "Non-default value of setting." } + }, + "labs:dashboard:enable_ui": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, + "labs:dashboard:deferBelowFold": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/src/plugins/timelion/public/components/timelion_deprecation.tsx b/src/plugins/timelion/public/components/timelion_deprecation.tsx index 41ae09f30586341..efcef88b3d0a26c 100644 --- a/src/plugins/timelion/public/components/timelion_deprecation.tsx +++ b/src/plugins/timelion/public/components/timelion_deprecation.tsx @@ -19,7 +19,7 @@ export const TimelionDeprecation = ({ links }: DocLinksStart) => { title={ diff --git a/src/plugins/timelion/server/deprecations.ts b/src/plugins/timelion/server/deprecations.ts index 3d4e687f154cfbf..e65d72cb460dfb0 100644 --- a/src/plugins/timelion/server/deprecations.ts +++ b/src/plugins/timelion/server/deprecations.ts @@ -30,7 +30,7 @@ export const showWarningMessageIfTimelionSheetWasFound = async ( const count = await getTimelionSheetsCount(savedObjectsClient); if (count > 0) { logger.warn( - 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' + 'Deprecated since 7.0, the Timelion app will be removed in the last 7.x minor version. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' ); } }; @@ -49,7 +49,7 @@ export async function getDeprecations({ if (count > 0) { deprecations.push({ - message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + message: `You have ${count} Timelion worksheets. The Timelion app will be removed in the last 7.x minor version. To continue using your Timelion worksheets, migrate them to a dashboard.`, documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', level: 'warning', diff --git a/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx index a6e0ee0d2aec443..1c721cde37b2b65 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx @@ -38,7 +38,7 @@ const defaultConfig = { from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, }, - validateClass: search.aggs.Ipv4Address, + validateClass: search.aggs.IpAddress, getModelValue: (item: FromToObject = {}) => ({ from: { value: item.from || EMPTY_STRING, diff --git a/src/plugins/vis_type_timeseries/common/enums/metric_types.ts b/src/plugins/vis_type_timeseries/common/enums/metric_types.ts index 17352f0f9da258b..8e2bc8f346eb60d 100644 --- a/src/plugins/vis_type_timeseries/common/enums/metric_types.ts +++ b/src/plugins/vis_type_timeseries/common/enums/metric_types.ts @@ -27,6 +27,7 @@ export enum METRIC_TYPES { // We should probably use BUCKET_TYPES from data plugin in future. export enum BUCKET_TYPES { TERMS = 'terms', + FILTERS = 'filters', } export const EXTENDED_STATS_TYPES = [ diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts index df0874fdd73ec24..ff7b2f497fd7093 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts @@ -15,6 +15,18 @@ jest.mock('../../../services', () => { getDataStart: jest.fn(() => { return { indexPatterns: jest.fn(), + query: { + timefilter: { + timefilter: { + getTime: jest.fn(() => { + return { + from: '2021-04-30T16:42:24.502Z', + to: '2021-05-05T14:42:24.502Z', + }; + }), + }, + }, + }, }; }), }; @@ -25,9 +37,12 @@ describe('convert series to datatables', () => { beforeEach(() => { const fieldMap: Record = { - test1: { name: 'test1', spec: { type: 'date' } } as IndexPatternField, - test2: { name: 'test2' } as IndexPatternField, - test3: { name: 'test3', spec: { type: 'boolean' } } as IndexPatternField, + test1: { name: 'test1', spec: { type: 'date', name: 'test1' } } as IndexPatternField, + test2: { + name: 'test2', + spec: { type: 'number', name: 'Average of test2' }, + } as IndexPatternField, + test3: { name: 'test3', spec: { type: 'boolean', name: 'test3' } } as IndexPatternField, }; const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name]; @@ -41,8 +56,8 @@ describe('convert series to datatables', () => { describe('addMetaColumns()', () => { test('adds the correct meta to a date column', () => { - const columns = [{ id: 0, name: 'test1', isSplit: false }]; - const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'count'); + const columns = [{ id: 0, name: 'test1', isMetric: true, type: 'date_histogram' }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern); expect(columnsWithMeta).toEqual([ { id: '0', @@ -54,6 +69,13 @@ describe('convert series to datatables', () => { enabled: true, indexPatternId: 'index1', type: 'date_histogram', + schema: 'metric', + params: { + timeRange: { + from: '2021-04-30T16:42:24.502Z', + to: '2021-05-05T14:42:24.502Z', + }, + }, }, type: 'date', }, @@ -63,8 +85,8 @@ describe('convert series to datatables', () => { }); test('adds the correct meta to a non date column', () => { - const columns = [{ id: 1, name: 'Average of test2', isSplit: false }]; - const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + const columns = [{ id: 1, name: 'test2', isMetric: true, type: 'avg' }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern); expect(columnsWithMeta).toEqual([ { id: '1', @@ -76,17 +98,21 @@ describe('convert series to datatables', () => { enabled: true, indexPatternId: 'index1', type: 'avg', + schema: 'metric', + params: { + field: 'Average of test2', + }, }, type: 'number', }, - name: 'Average of test2', + name: 'test2', }, ]); }); test('adds the correct meta for a split column', () => { - const columns = [{ id: 2, name: 'test3', isSplit: true }]; - const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + const columns = [{ id: 2, name: 'test3', isMetric: false, type: 'terms' }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern); expect(columnsWithMeta).toEqual([ { id: '2', @@ -98,6 +124,10 @@ describe('convert series to datatables', () => { enabled: true, indexPatternId: 'index1', type: 'terms', + schema: 'group', + params: { + field: 'test3', + }, }, type: 'boolean', }, diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts index 35506f9e6208105..19a1910afbe2fc2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts @@ -6,51 +6,74 @@ * Side Public License, v 1. */ import { IndexPattern } from 'src/plugins/data/public'; -import { - Datatable, - DatatableRow, - DatatableColumn, - DatatableColumnType, -} from 'src/plugins/expressions/public'; +import { DatatableRow, DatatableColumn, DatatableColumnType } from 'src/plugins/expressions/public'; +import { Query } from 'src/plugins/data/common'; import { TimeseriesVisParams } from '../../../types'; import type { PanelData } from '../../../../common/types'; +import { BUCKET_TYPES } from '../../../../common/enums'; import { fetchIndexPattern } from '../../../../common/index_patterns_utils'; import { getDataStart } from '../../../services'; import { X_ACCESSOR_INDEX } from '../../visualizations/constants'; +import type { TSVBTables } from './types'; -interface TSVBTables { - [key: string]: Datatable; +interface FilterParams { + filter?: Query; + label?: string; + field?: string; } interface TSVBColumns { id: number; name: string; - isSplit: boolean; + isMetric: boolean; + type: string; + params?: FilterParams[]; } export const addMetaToColumns = ( columns: TSVBColumns[], - indexPattern: IndexPattern, - metricsType: string + indexPattern: IndexPattern ): DatatableColumn[] => { return columns.map((column) => { const field = indexPattern.getFieldByName(column.name); const type = (field?.spec.type as DatatableColumnType) || 'number'; + + let params: unknown = { + field: field?.spec.name, + }; + if (column.type === BUCKET_TYPES.FILTERS && column.params) { + const filters = column.params.map((col) => ({ + input: col.filter, + label: col.label, + })); + params = { + filters, + }; + } else if (column.type === 'date_histogram') { + const { query } = getDataStart(); + const timeRange = query.timefilter.timefilter.getTime(); + params = { + timeRange, + }; + } + const cleanedColumn = { id: column.id.toString(), name: column.name, meta: { type, - field: column.name, + field: field?.spec.name, index: indexPattern.title, source: 'esaggs', sourceParams: { enabled: true, indexPatternId: indexPattern?.id, - type: type === 'date' ? 'date_histogram' : column.isSplit ? 'terms' : metricsType, + type: column.type, + schema: column.isMetric ? 'metric' : 'group', + params, }, }, - }; + } as DatatableColumn; return cleanedColumn; }); }; @@ -73,30 +96,58 @@ export const convertSeriesToDataTable = async ( usedIndexPattern = indexPattern; } } - const isGroupedByTerms = layer.split_mode === 'terms'; + const isGroupedByTerms = layer.split_mode === BUCKET_TYPES.TERMS; + const isGroupedByFilters = layer.split_mode === BUCKET_TYPES.FILTERS; const seriesPerLayer = series.filter((s) => s.seriesId === layer.id); let id = X_ACCESSOR_INDEX; const columns: TSVBColumns[] = [ - { id, name: usedIndexPattern.timeFieldName || '', isSplit: false }, + { + id, + name: usedIndexPattern.timeFieldName || '', + isMetric: false, + type: 'date_histogram', + }, ]; + if (seriesPerLayer.length) { id++; - columns.push({ id, name: seriesPerLayer[0].splitByLabel, isSplit: false }); - // Adds an extra column, if the layer is split by terms aggregation + const metrics = layer.metrics; + columns.push({ + id, + name: metrics[metrics.length - 1].field || seriesPerLayer[0].splitByLabel, + isMetric: true, + type: metrics[metrics.length - 1].type, + }); + + // Adds an extra column, if the layer is split by terms or filters aggregation if (isGroupedByTerms) { id++; - columns.push({ id, name: layer.terms_field || '', isSplit: true }); + columns.push({ + id, + name: layer.terms_field || '', + isMetric: false, + type: BUCKET_TYPES.TERMS, + }); + } else if (isGroupedByFilters) { + id++; + columns.push({ + id, + name: BUCKET_TYPES.FILTERS, + isMetric: false, + params: layer?.split_filters as FilterParams[], + type: BUCKET_TYPES.FILTERS, + }); } } - const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern, layer.metrics[0].type); - + const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern); + const filtersColumn = columns.find((col) => col.type === BUCKET_TYPES.FILTERS); let rows: DatatableRow[] = []; for (let j = 0; j < seriesPerLayer.length; j++) { const data = seriesPerLayer[j].data.map((rowData) => { const row: DatatableRow = [rowData[0], rowData[1]]; // If the layer is split by terms aggregation, the data array should also contain the split value. - if (isGroupedByTerms) { + if (isGroupedByTerms || filtersColumn) { row.push(seriesPerLayer[j].label); } return row; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts new file mode 100644 index 000000000000000..2b9a1f930c15a1f --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; +import { getClickFilterData } from './get_click_filter_data'; +import type { TSVBTables } from './types'; +import { TimeseriesVisParams } from '../../../types'; + +describe('getClickFilterData', () => { + test('gets the correct data for a group by everything timeseries chart', () => { + const points = [ + [ + { + x: 1620032400000, + y: 72, + accessor: 'y1', + datum: [1620032400000, 72], + }, + { + key: + 'groupId{yaxis_50589930-ad98-11eb-b6f6-59011d7388b7_main_group}spec{61ca57f1-469d-11e7-af02-69e470af7417}yAccessor{1}splitAccessors{}', + specId: '61ca57f1-469d-11e7-af02-69e470af7417', + yAccessor: 1, + }, + ], + ] as Array<[GeometryValue, XYChartSeriesIdentifier]>; + const tables = { + '61ca57f1-469d-11e7-af02-69e470af7417': { + type: 'datatable', + rows: [ + [1620010800000, 8], + [1620021600000, 33], + [1620032400000, 72], + [1620043200000, 66], + [1620054000000, 36], + [1620064800000, 11], + ], + columns: [ + { + id: '0', + name: 'timestamp', + meta: { + type: 'date', + field: 'timestamp', + index: 'kibana_sample_data_logs', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'date_histogram', + schema: 'group', + params: { + field: 'timestamp', + }, + }, + }, + }, + { + id: '1', + name: 'Count', + meta: { + type: 'number', + index: 'kibana_sample_data_logs', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'count', + schema: 'metric', + params: {}, + }, + }, + }, + ], + }, + } as TSVBTables; + const model = { + series: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417', + split_mode: 'everything', + }, + ], + } as TimeseriesVisParams; + const data = getClickFilterData(points, tables, model); + expect(data[0].column).toEqual(0); + expect(data[0].row).toEqual(2); + expect(data[0].value).toEqual(points[0][0].x); + }); + + test('gets the correct data for a group by terms timeseries chart', () => { + const points = [ + [ + { + x: 1619481600000, + y: 3, + accessor: 'y1', + datum: [1619481600000, 3], + }, + { + key: + 'groupId{yaxis_6e0353a0-ad9b-11eb-b112-89cce8e43380_main_group}spec{61ca57f1-469d-11e7-af02-69e470af7417:1}yAccessor{1}splitAccessors{}', + specId: '61ca57f1-469d-11e7-af02-69e470af7417:1', + }, + ], + ] as Array<[GeometryValue, XYChartSeriesIdentifier]>; + const tables = { + '61ca57f1-469d-11e7-af02-69e470af7417': { + type: 'datatable', + rows: [ + [1619449200000, 31, 0], + [1619460000000, 36, 0], + [1619470800000, 35, 0], + [1619481600000, 40, 0], + [1619492400000, 38, 0], + [1619503200000, 30, 0], + [1620172800000, 0, 0], + [1619449200000, 4, 1], + [1619460000000, 4, 1], + [1619470800000, 3, 1], + [1619481600000, 3, 1], + [1619492400000, 2, 1], + [1619503200000, 3, 1], + ], + columns: [ + { + id: '0', + name: 'timestamp', + meta: { + type: 'date', + field: 'timestamp', + index: 'kibana_sample_data_flights', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + type: 'date_histogram', + schema: 'group', + params: { + field: 'timestamp', + }, + }, + }, + }, + { + id: '1', + name: 'Count', + meta: { + type: 'number', + index: 'kibana_sample_data_flights', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + type: 'count', + schema: 'metric', + params: {}, + }, + }, + }, + { + id: '2', + name: 'Cancelled', + meta: { + type: 'boolean', + field: 'Cancelled', + index: 'kibana_sample_data_flights', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + type: 'terms', + schema: 'group', + params: { + field: 'Cancelled', + }, + }, + }, + }, + ], + }, + } as TSVBTables; + const model = { + series: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417', + split_mode: 'terms', + }, + ], + } as TimeseriesVisParams; + const data = getClickFilterData(points, tables, model); + expect(data.length).toEqual(2); + expect(data[0].column).toEqual(0); + expect(data[0].row).toEqual(10); + expect(data[0].value).toEqual(points[0][0].x); + + expect(data[1].column).toEqual(2); + expect(data[1].row).toEqual(10); + // expect(data).toEqual([]); + const splitValue = points[0][1].specId.split(':'); + expect(data[1].value).toEqual(parseInt(splitValue[1], 10)); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts new file mode 100644 index 000000000000000..a6e35aa6e10328a --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; +import { ValueClickContext } from 'src/plugins/embeddable/public'; +import { X_ACCESSOR_INDEX } from '../../visualizations/constants'; +import { BUCKET_TYPES } from '../../../../common/enums'; +import { TimeseriesVisParams } from '../../../types'; +import type { TSVBTables } from './types'; + +export const getClickFilterData = ( + points: Array<[GeometryValue, XYChartSeriesIdentifier]>, + tables: TSVBTables, + model: TimeseriesVisParams +) => { + const data: ValueClickContext['data']['data'] = []; + points.forEach((point) => { + const [geometry] = point; + const { specId } = point[1]; + // specId for a split series has the format + // 61ca57f1-469d-11e7-af02-69e470af7417:Men's Accessories, : + const [layerId, splitLabel] = specId.split(':'); + const table = tables[layerId]; + + const layer = model.series.filter(({ id }) => id === layerId); + let label = splitLabel; + // compute label for filters split mode + if (splitLabel && layer.length && layer[0].split_mode === BUCKET_TYPES.FILTERS) { + const filter = layer[0]?.split_filters?.filter(({ id }) => id === splitLabel); + label = filter?.[0].label || (filter?.[0].filter?.query as string); + } + const index = table.rows.findIndex((row) => { + const condition = + geometry.x === row[X_ACCESSOR_INDEX] && geometry.y === row[X_ACCESSOR_INDEX + 1]; + return splitLabel ? condition && row[X_ACCESSOR_INDEX + 2].toString() === label : condition; + }); + if (index < 0) return; + + // Filter out the metric column + const bucketCols = table.columns.filter((col) => col.meta.sourceParams?.schema === 'group'); + + const newData = bucketCols.map(({ id }) => ({ + table, + column: parseInt(id, 10), + row: index, + value: table.rows[index][id] ?? null, + })); + if (newData.length) { + data.push(...newData); + } + }); + return data; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts new file mode 100644 index 000000000000000..fee6670b49b9ceb --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { Datatable } from 'src/plugins/expressions/public'; + +export interface TSVBTables { + [key: string]: Datatable; +} 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 9683089094054bf..5391bf319ee5739 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 @@ -12,7 +12,7 @@ import React, { useCallback, useEffect } from 'react'; import { get } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - +import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; import { IUiSettingsClient } from 'src/core/public'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; @@ -27,6 +27,7 @@ import { fetchIndexPattern } from '../../../common/index_patterns_utils'; import { TimeseriesVisParams } from '../../types'; import { getDataStart } from '../../services'; import { convertSeriesToDataTable } from './lib/convert_series_to_datatable'; +import { getClickFilterData } from './lib/get_click_filter_data'; import { X_ACCESSOR_INDEX } from '../visualizations/constants'; import { LastValueModeIndicator } from './last_value_mode_indicator'; import { getInterval } from './lib/get_interval'; @@ -102,6 +103,37 @@ function TimeseriesVisualization({ [handlers, model] ); + const handleFilterClick = useCallback( + async (series: PanelData[], points: Array<[GeometryValue, XYChartSeriesIdentifier]>) => { + const indexPatternValue = model.index_pattern || ''; + const { indexPatterns } = getDataStart(); + const { indexPattern } = await fetchIndexPattern(indexPatternValue, indexPatterns); + + // it should work only if index pattern is found + if (!indexPattern) return; + + const tables = indexPattern + ? await convertSeriesToDataTable(model, series, indexPattern) + : null; + + if (!tables) return; + + const data = getClickFilterData(points, tables, model); + + const event = { + name: 'filterBucket', + data: { + data, + negate: false, + timeFieldName: indexPattern.timeFieldName, + }, + }; + + handlers.event(event); + }, + [handlers, model] + ); + const handleUiState = useCallback( (field: string, value: { column: string; order: string }) => { uiState.set(field, value); @@ -156,6 +188,7 @@ function TimeseriesVisualization({ visData={visData} uiState={uiState} onBrush={onBrush} + onFilterClick={handleFilterClick} onUiState={handleUiState} syncColors={syncColors} palettesService={palettesService} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index de3163c44f95ba5..99f25643284025a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -8,6 +8,7 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; +import uuid from 'uuid/v4'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; import { EventEmitter } from 'events'; @@ -79,6 +80,9 @@ export class VisEditor extends Component Promise; + onFilterClick: ( + series: PanelData[], + points: Array<[GeometryValue, XYChartSeriesIdentifier]> + ) => Promise; onUiState: ( field: string, value: { 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 6510ad24615a6cc..ef6b30be30a30e6 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 @@ -91,6 +91,7 @@ MarkdownVisualization.propTypes = { className: PropTypes.string, model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index 2b58145a4928d7c..3029bba04b45002 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -79,6 +79,7 @@ MetricVisualization.propTypes = { additionalLabel: PropTypes.string, model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, 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 29560c4bd9368bb..8e59e8e1bb628ab 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 @@ -26,6 +26,7 @@ class TimeseriesVisualization extends Component { static propTypes = { model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, }; @@ -136,7 +137,7 @@ class TimeseriesVisualization extends Component { }; render() { - const { model, visData, onBrush, syncColors, palettesService } = this.props; + const { model, visData, onBrush, onFilterClick, syncColors, palettesService } = this.props; const series = get(visData, `${model.id}.series`, []); const interval = getInterval(visData, model); const yAxisIdGenerator = htmlIdGenerator('yaxis'); @@ -230,6 +231,7 @@ class TimeseriesVisualization extends Component { series={series} yAxis={yAxis} onBrush={onBrush} + onFilterClick={onFilterClick} backgroundColor={model.background_color} showGrid={Boolean(model.show_grid)} legend={Boolean(model.show_legend)} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index 8150d2e4d6e1b41..41e6236cbc39b2a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -96,6 +96,7 @@ TopNVisualization.propTypes = { className: PropTypes.string, model: PropTypes.object, onBrush: PropTypes.func, + onFilterClick: PropTypes.func, onChange: PropTypes.func, visData: PropTypes.object, getConfig: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index 7dc6a26185e2bfb..4b933bc81d8825e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -109,6 +109,7 @@ export function visWithSplits(WrappedComponent) { model={model} visData={newVisData} onBrush={props.onBrush} + onFilterClick={props.onFilterClick} additionalLabel={additionalLabel || emptyLabel} backgroundColor={props.backgroundColor} getConfig={props.getConfig} diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 44c1742e144be15..a4d834ea8d2175d 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -61,6 +61,7 @@ export const TimeSeries = ({ series, yAxis, onBrush, + onFilterClick, xAxisFormatter, annotations, syncColors, @@ -118,6 +119,10 @@ export const TimeSeries = ({ onBrush(min, max, series); }; + const handleElementClick = (points) => { + onFilterClick(series, points); + }; + const getSeriesColor = useCallback( (seriesName, seriesGroupId, seriesId) => { const seriesById = series.filter((s) => s.seriesId === seriesGroupId); @@ -142,6 +147,7 @@ export const TimeSeries = ({ showLegendExtra={true} legendPosition={legendPosition} onBrushEnd={onBrushEndListener} + onElementClick={(args) => handleElementClick(args)} animateData={false} onPointerUpdate={handleCursorUpdate} theme={[ diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 66128b2abe856f7..3cb4faaacf25bea 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; - +import uuid from 'uuid/v4'; import { TSVB_EDITOR_NAME } from './application/editor_controller'; import { PANEL_TYPES, TOOLTIP_MODES } from '../common/enums'; import { isStringTypeIndexPattern } from '../common/index_patterns_utils'; @@ -25,11 +25,11 @@ export const metricsVisDefinition = { group: VisGroups.PROMOTED, visConfig: { defaults: { - id: '61ca57f0-469d-11e7-af02-69e470af7417', + id: uuid(), type: PANEL_TYPES.TIMESERIES, series: [ { - id: '61ca57f1-469d-11e7-af02-69e470af7417', + id: uuid(), color: '#68BC00', split_mode: 'everything', palette: { @@ -38,7 +38,7 @@ export const metricsVisDefinition = { }, metrics: [ { - id: '61ca57f2-469d-11e7-af02-69e470af7417', + id: uuid(), type: 'count', }, ], @@ -75,7 +75,7 @@ export const metricsVisDefinition = { }, toExpressionAst, getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.brush]; + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; }, inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 1c1212add3d8ce5..479c3f47435cc81 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -15,7 +15,6 @@ import { EditorController, TSVB_EDITOR_NAME } from './application/editor_control import { createMetricsFn } from './metrics_fn'; import { metricsVisDefinition } from './metrics_type'; import { - setSavedObjectsClient, setUISettings, setI18n, setFieldFormats, @@ -65,7 +64,6 @@ export class MetricsPlugin implements Plugin { } public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { - setSavedObjectsClient(core.savedObjects); setI18n(core.i18n); setFieldFormats(data.fieldFormats); setDataStart(data); diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts index 17c4a6c7de15321..22f99f95691dedd 100644 --- a/src/plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; +import { I18nStart, IUiSettingsClient, CoreStart } from 'src/core/public'; import { createGetterSetter } from '../../kibana_utils/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; @@ -17,10 +17,6 @@ export const [getFieldFormats, setFieldFormats] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('FieldFormats'); -export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( - 'SavedObjectsClient' -); - export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); export const [getDataStart, setDataStart] = createGetterSetter('DataStart'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index 29cf3f274dc24a7..f82f332df19fd12 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -68,6 +68,7 @@ export function dateHistogram( bucketSize, seriesId: series.id, index: panel.use_kibana_indexes ? seriesIndex.indexPattern?.id : undefined, + panelId: panel.id, }); return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index a1fd242dc150e8e..741eb93267f4c25 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -32,6 +32,7 @@ describe('dateHistogram(req, panel, series)', () => { index_pattern: '*', time_field: '@timestamp', interval: '10s', + id: 'panelId', }; series = { id: 'test' }; config = { @@ -89,6 +90,7 @@ describe('dateHistogram(req, panel, series)', () => { intervalString: '10s', timeField: '@timestamp', seriesId: 'test', + panelId: 'panelId', }, }, }, @@ -130,6 +132,7 @@ describe('dateHistogram(req, panel, series)', () => { intervalString: '10s', timeField: '@timestamp', seriesId: 'test', + panelId: 'panelId', }, }, }, @@ -174,6 +177,7 @@ describe('dateHistogram(req, panel, series)', () => { intervalString: '20s', timeField: 'timestamp', seriesId: 'test', + panelId: 'panelId', }, }, }, @@ -230,6 +234,7 @@ describe('dateHistogram(req, panel, series)', () => { seriesId: 'test', bucketSize: 10, intervalString: '10s', + panelId: 'panelId', }, }, }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js index 0194e1ce3e5d969..a3a9022a67191a1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js @@ -11,6 +11,7 @@ import { normalizeQuery } from './normalize_query'; describe('normalizeQuery', () => { const req = 'req'; const seriesId = '61ca57f1-469d-11e7-af02-69e470af7417'; + const panelId = '39d49073-a924-426b-aa32-35acb40a9bb7'; let next; let panel; @@ -48,6 +49,7 @@ describe('normalizeQuery', () => { intervalString: '10s', bucketSize: 10, seriesId: [seriesId], + panelId, }, }, }, @@ -78,6 +80,7 @@ describe('normalizeQuery', () => { intervalString: '10s', bucketSize: 10, seriesId: [seriesId], + panelId: panelId, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index f0989cf0fa08b4e..3e883abc9e5e087 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -24,6 +24,7 @@ export function dateHistogram(req, panel, esQueryConfig, seriesIndex, capabiliti const meta = { timeField, index: panel.use_kibana_indexes ? seriesIndex.indexPattern?.id : undefined, + panelId: panel.id, }; const getDateHistogramForLastBucketMode = () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js index e2a7e7b17b548b6..71381f32599e354 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.js @@ -11,6 +11,7 @@ import { normalizeQuery } from './normalize_query'; describe('normalizeQuery', () => { const req = 'req'; const seriesId = '61ca57f1-469d-11e7-af02-69e470af7417'; + const panelId = '39d49073-a924-426b-aa32-35acb40a9bb7'; let next; let panel; @@ -56,6 +57,7 @@ describe('normalizeQuery', () => { timeField: 'order_date', intervalString: '10s', bucketSize: 10, + panelId, }, }, }, @@ -87,6 +89,7 @@ describe('normalizeQuery', () => { timeField: 'order_date', intervalString: '10s', bucketSize: 10, + panelId, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index 89d87da5f3d7e3a..5b865d451003a99 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -157,6 +157,7 @@ describe('buildRequestBody(req)', () => { intervalString: '10s', seriesId: 'c9b5f9c0-e403-11e6-be91-6f7688e9fac7', timeField: '@timestamp', + panelId: 'c9b5d2b0-e403-11e6-be91-6f7688e9fac7', }, }, }, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index d5a67f2eac0644a..4fd19aa45e69e72 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -28,9 +28,12 @@ import { setUISettings, } from '../../services'; import { initVegaLayer, initTmsRasterLayer } from './layers'; -import { Map, NavigationControl, Style } from 'mapbox-gl'; -jest.mock('mapbox-gl', () => ({ +// @ts-expect-error +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; + +jest.mock('mapbox-gl/dist/mapbox-gl-csp', () => ({ + setRTLTextPlugin: jest.fn(), Map: jest.fn().mockImplementation(() => ({ getLayer: () => '', removeLayer: jest.fn(), @@ -75,9 +78,10 @@ describe('vega_map_view/view', () => { setUISettings(coreStart.uiSettings); const getTmsService = jest.fn().mockReturnValue(({ - getVectorStyleSheet: (): Style => ({ + getVectorStyleSheet: () => ({ version: 8, sources: {}, + // @ts-expect-error layers: [], }), getMaxZoom: async () => 20, @@ -144,7 +148,7 @@ describe('vega_map_view/view', () => { await vegaMapView.init(); const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig; - expect(Map).toHaveBeenCalledWith({ + expect(mapboxgl.Map).toHaveBeenCalledWith({ style: { version: 8, sources: {}, @@ -170,7 +174,7 @@ describe('vega_map_view/view', () => { await vegaMapView.init(); const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig; - expect(Map).toHaveBeenCalledWith({ + expect(mapboxgl.Map).toHaveBeenCalledWith({ style: { version: 8, sources: {}, @@ -195,7 +199,7 @@ describe('vega_map_view/view', () => { await vegaMapView.init(); - expect(NavigationControl).toHaveBeenCalled(); + expect(mapboxgl.NavigationControl).toHaveBeenCalled(); }); }); }); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 453e9596a2a4c69..e899057819a1925 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -7,9 +7,11 @@ */ import { i18n } from '@kbn/i18n'; -import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; +import type { Map, Style, MapboxOptions } from 'mapbox-gl'; import { View, parse } from 'vega'; +// @ts-expect-error +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -22,11 +24,17 @@ import { userConfiguredLayerId, vegaLayerId, } from './constants'; - import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; - import './vega_map_view.scss'; +// @ts-expect-error +import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; +// @ts-expect-error +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; + +mapboxgl.workerUrl = mbWorkerUrl; +mapboxgl.setRTLTextPlugin(mbRtlPlugin); + async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); @@ -115,7 +123,7 @@ export class VegaMapView extends VegaBaseView { // In some cases, Vega may be initialized twice, e.g. after awaiting... if (!this._$container) return; - const mapBoxInstance = new Map({ + const mapBoxInstance = new mapboxgl.Map({ style, customAttribution, container: this._$container.get(0), @@ -142,7 +150,7 @@ export class VegaMapView extends VegaBaseView { private initControls(mapBoxInstance: Map) { if (this.shouldShowZoomControl) { - mapBoxInstance.addControl(new NavigationControl({ showCompass: false }), 'top-left'); + mapBoxInstance.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); } // disable map rotation using right click + drag diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts index d5f8d978d525215..310486bfdfffded 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts @@ -7,7 +7,7 @@ */ import { parse } from 'hjson'; -import { ElasticsearchClient, SavedObject } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; @@ -27,7 +27,7 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home']) const sampleDataSets = home?.sampleData.getSampleDatasets() ?? []; sampleDataSets.forEach((sampleDataSet) => - sampleDataSet.savedObjects.forEach((savedObject: SavedObject) => { + sampleDataSet.savedObjects.forEach((savedObject) => { try { if (savedObject.type === 'visualization') { const visState = JSON.parse(savedObject.attributes?.visState); diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap index c3ffc0dd08412df..3ca2834a54fca2f 100644 --- a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap @@ -8,7 +8,7 @@ Object { "area", ], "visConfig": Array [ - "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"palette\\":{\\"name\\":\\"default\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", ], }, "getArgument": [Function], diff --git a/src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html b/src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html deleted file mode 100644 index ee95eef68f3b27f..000000000000000 --- a/src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html +++ /dev/null @@ -1,7 +0,0 @@ -

- - - <%= wholeBucket ? 'Part of this bucket' : 'This area' %> - may contain partial data. The selected time range does not fully cover it. - -

diff --git a/src/plugins/vis_type_vislib/public/vislib/partials/touchdown_template.tsx b/src/plugins/vis_type_vislib/public/vislib/partials/touchdown_template.tsx new file mode 100644 index 000000000000000..55955da07ebdd05 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/partials/touchdown_template.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom/server'; + +interface Props { + wholeBucket: boolean; +} + +export const touchdownTemplate = ({ wholeBucket }: Props) => { + return ReactDOM.renderToStaticMarkup( +

+ + + {wholeBucket ? 'Part of this bucket' : 'This area'} may contain partial data. The selected + time range does not fully cover it. + +

+ ); +}; diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js index 28464009a8339ae..b4ab2ea2992c5cd 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js @@ -14,10 +14,10 @@ import { Tooltip } from '../components/tooltip'; import { Chart } from './_chart'; import { TimeMarker } from './time_marker'; import { seriesTypes } from './point_series/series_types'; -import touchdownTmplHtml from '../partials/touchdown.tmpl.html'; +import { touchdownTemplate } from '../partials/touchdown_template'; const seriTypes = seriesTypes; -const touchdownTmpl = _.template(touchdownTmplHtml); + /** * Line Chart Visualization * @@ -169,7 +169,7 @@ export class PointSeries extends Chart { } function textFormatter() { - return touchdownTmpl(callPlay(d3.event)); + return touchdownTemplate(callPlay(d3.event)); } const endzoneTT = new Tooltip('endzones', this.handler.el, textFormatter, null); diff --git a/src/plugins/vis_type_xy/common/index.ts b/src/plugins/vis_type_xy/common/index.ts index 903adb53eb403ec..a80946f7c62fa39 100644 --- a/src/plugins/vis_type_xy/common/index.ts +++ b/src/plugins/vis_type_xy/common/index.ts @@ -6,17 +6,14 @@ * Side Public License, v 1. */ -import { $Values } from '@kbn/utility-types'; - /** * Type of charts able to render */ -export const ChartType = Object.freeze({ - Line: 'line' as const, - Area: 'area' as const, - Histogram: 'histogram' as const, -}); -export type ChartType = $Values; +export enum ChartType { + Line = 'line', + Area = 'area', + Histogram = 'histogram', +} /** * Type of xy visualizations diff --git a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap index e6665c26a28150f..7c21e699216bc3d 100644 --- a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap @@ -4,11 +4,70 @@ exports[`xy vis toExpressionAst function should match basic snapshot 1`] = ` Object { "addArgument": [Function], "arguments": Object { + "addLegend": Array [ + true, + ], + "addTimeMarker": Array [ + false, + ], + "addTooltip": Array [ + true, + ], + "categoryAxes": Array [ + Object { + "toAst": [Function], + }, + ], + "chartType": Array [ + "area", + ], + "gridCategoryLines": Array [ + false, + ], + "labels": Array [ + Object { + "toAst": [Function], + }, + ], + "legendPosition": Array [ + "top", + ], + "palette": Array [ + "default", + ], + "seriesDimension": Array [ + Object { + "toAst": [Function], + }, + ], + "seriesParams": Array [ + Object { + "toAst": [Function], + }, + ], + "thresholdLine": Array [ + Object { + "toAst": [Function], + }, + ], + "times": Array [], "type": Array [ "area", ], - "visConfig": Array [ - "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "valueAxes": Array [ + Object { + "toAst": [Function], + }, + ], + "xDimension": Array [ + Object { + "toAst": [Function], + }, + ], + "yDimension": Array [ + Object { + "toAst": [Function], + }, ], }, "getArgument": [Function], diff --git a/src/plugins/vis_type_xy/public/editor/common_config.tsx b/src/plugins/vis_type_xy/public/editor/common_config.tsx index 1e4ac7df0082c7f..1815d9cfc429da2 100644 --- a/src/plugins/vis_type_xy/public/editor/common_config.tsx +++ b/src/plugins/vis_type_xy/public/editor/common_config.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisEditorOptionsProps } from '../../../visualizations/public'; +import type { VisEditorOptionsProps } from '../../../visualizations/public'; -import { VisParams } from '../types'; +import type { VisParams } from '../types'; import { MetricsAxisOptions, PointSeriesOptions } from './components/options'; -import { ValidationWrapper } from './components/common'; +import { ValidationWrapper } from './components/common/validation_wrapper'; export function getOptionTabs(showElasticChartsOptions = false) { return [ diff --git a/src/plugins/vis_type_xy/public/expression_functions/category_axis.ts b/src/plugins/vis_type_xy/public/expression_functions/category_axis.ts new file mode 100644 index 000000000000000..30215d8feb8a30a --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/category_axis.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { CategoryAxis } from '../types'; +import type { ExpressionValueScale } from './vis_scale'; +import type { ExpressionValueLabel } from './label'; + +export interface Arguments extends Omit { + title?: string; + scale: ExpressionValueScale; + labels: ExpressionValueLabel; +} + +export type ExpressionValueCategoryAxis = ExpressionValueBoxed< + 'category_axis', + { + id: CategoryAxis['id']; + show: CategoryAxis['show']; + position: CategoryAxis['position']; + axisType: CategoryAxis['type']; + title: { + text?: string; + }; + labels: CategoryAxis['labels']; + scale: CategoryAxis['scale']; + } +>; + +export const categoryAxis = (): ExpressionFunctionDefinition< + 'categoryaxis', + Datatable | null, + Arguments, + ExpressionValueCategoryAxis +> => ({ + name: 'categoryaxis', + help: i18n.translate('visTypeXy.function.categoryAxis.help', { + defaultMessage: 'Generates category axis object', + }), + type: 'category_axis', + args: { + id: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.id.help', { + defaultMessage: 'Id of category axis', + }), + required: true, + }, + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.categoryAxis.show.help', { + defaultMessage: 'Show the category axis', + }), + required: true, + }, + position: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.position.help', { + defaultMessage: 'Position of the category axis', + }), + required: true, + }, + type: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.type.help', { + defaultMessage: 'Type of the category axis. Can be category or value', + }), + required: true, + }, + title: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.title.help', { + defaultMessage: 'Title of the category axis', + }), + }, + scale: { + types: ['vis_scale'], + help: i18n.translate('visTypeXy.function.categoryAxis.scale.help', { + defaultMessage: 'Scale config', + }), + }, + labels: { + types: ['label'], + help: i18n.translate('visTypeXy.function.categoryAxis.labels.help', { + defaultMessage: 'Axis label config', + }), + }, + }, + fn: (context, args) => { + return { + type: 'category_axis', + id: args.id, + show: args.show, + position: args.position, + axisType: args.type, + title: { + text: args.title, + }, + scale: { + ...args.scale, + type: args.scale.scaleType, + }, + labels: args.labels, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/index.ts b/src/plugins/vis_type_xy/public/expression_functions/index.ts new file mode 100644 index 000000000000000..4e7db57ee65d759 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { visTypeXyVisFn } from './xy_vis_fn'; + +export { categoryAxis, ExpressionValueCategoryAxis } from './category_axis'; +export { timeMarker, ExpressionValueTimeMarker } from './time_marker'; +export { valueAxis, ExpressionValueValueAxis } from './value_axis'; +export { seriesParam, ExpressionValueSeriesParam } from './series_param'; +export { thresholdLine, ExpressionValueThresholdLine } from './threshold_line'; +export { label, ExpressionValueLabel } from './label'; +export { visScale, ExpressionValueScale } from './vis_scale'; +export { xyDimension, ExpressionValueXYDimension } from './xy_dimension'; diff --git a/src/plugins/vis_type_xy/public/expression_functions/label.ts b/src/plugins/vis_type_xy/public/expression_functions/label.ts new file mode 100644 index 000000000000000..934278d13cff02f --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/label.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { Labels } from '../../../charts/public'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; + +export type ExpressionValueLabel = ExpressionValueBoxed< + 'label', + { + color?: Labels['color']; + filter?: Labels['filter']; + overwriteColor?: Labels['overwriteColor']; + rotate?: Labels['rotate']; + show?: Labels['show']; + truncate?: Labels['truncate']; + } +>; + +export const label = (): ExpressionFunctionDefinition< + 'label', + Datatable | null, + Labels, + ExpressionValueLabel +> => ({ + name: 'label', + help: i18n.translate('visTypeXy.function.label.help', { + defaultMessage: 'Generates label object', + }), + type: 'label', + args: { + color: { + types: ['string'], + help: i18n.translate('visTypeXy.function.label.color.help', { + defaultMessage: 'Color of label', + }), + }, + filter: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.label.filter.help', { + defaultMessage: 'Hides overlapping labels and duplicates on axis', + }), + }, + overwriteColor: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.label.overwriteColor.help', { + defaultMessage: 'Overwrite color', + }), + }, + rotate: { + types: ['number'], + help: i18n.translate('visTypeXy.function.label.rotate.help', { + defaultMessage: 'Rotate angle', + }), + }, + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.label.show.help', { + defaultMessage: 'Show label', + }), + }, + truncate: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.label.truncate.help', { + defaultMessage: 'The number of symbols before truncating', + }), + }, + }, + fn: (context, args) => { + return { + type: 'label', + color: args.color, + filter: args.hasOwnProperty('filter') ? args.filter : undefined, + overwriteColor: args.overwriteColor, + rotate: args.rotate, + show: args.show, + truncate: args.truncate, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/series_param.ts b/src/plugins/vis_type_xy/public/expression_functions/series_param.ts new file mode 100644 index 000000000000000..402187cea65866b --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/series_param.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { SeriesParam } from '../types'; + +export interface Arguments extends Omit { + label: string; + id: string; +} + +export type ExpressionValueSeriesParam = ExpressionValueBoxed< + 'series_param', + { + data: { label: string; id: string }; + drawLinesBetweenPoints?: boolean; + interpolate?: SeriesParam['interpolate']; + lineWidth?: number; + mode: SeriesParam['mode']; + show: boolean; + showCircles: boolean; + seriesParamType: SeriesParam['type']; + valueAxis: string; + } +>; + +export const seriesParam = (): ExpressionFunctionDefinition< + 'seriesparam', + Datatable, + Arguments, + ExpressionValueSeriesParam +> => ({ + name: 'seriesparam', + help: i18n.translate('visTypeXy.function.seriesparam.help', { + defaultMessage: 'Generates series param object', + }), + type: 'series_param', + inputTypes: ['datatable'], + args: { + label: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.label.help', { + defaultMessage: 'Name of series param', + }), + required: true, + }, + id: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.id.help', { + defaultMessage: 'Id of series param', + }), + required: true, + }, + drawLinesBetweenPoints: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.seriesParam.drawLinesBetweenPoints.help', { + defaultMessage: 'Draw lines between points', + }), + }, + interpolate: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.interpolate.help', { + defaultMessage: 'Interpolate mode. Can be linear, cardinal or step-after', + }), + }, + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.seriesParam.show.help', { + defaultMessage: 'Show param', + }), + required: true, + }, + lineWidth: { + types: ['number'], + help: i18n.translate('visTypeXy.function.seriesParam.lineWidth.help', { + defaultMessage: 'Width of line', + }), + }, + mode: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.mode.help', { + defaultMessage: 'Chart mode. Can be stacked or percentage', + }), + }, + showCircles: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.seriesParam.showCircles.help', { + defaultMessage: 'Show circles', + }), + }, + type: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.type.help', { + defaultMessage: 'Chart type. Can be line, area or histogram', + }), + }, + valueAxis: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.valueAxis.help', { + defaultMessage: 'Name of value axis', + }), + }, + }, + fn: (context, args) => { + return { + type: 'series_param', + data: { label: args.label, id: args.id }, + drawLinesBetweenPoints: args.drawLinesBetweenPoints, + interpolate: args.interpolate, + lineWidth: args.lineWidth, + mode: args.mode, + show: args.show, + showCircles: args.showCircles, + seriesParamType: args.type, + valueAxis: args.valueAxis, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts b/src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts new file mode 100644 index 000000000000000..8c01e375039850f --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { ThresholdLine } from '../types'; + +export type ExpressionValueThresholdLine = ExpressionValueBoxed< + 'threshold_line', + { + show: ThresholdLine['show']; + value: ThresholdLine['value']; + width: ThresholdLine['width']; + style: ThresholdLine['style']; + color: ThresholdLine['color']; + } +>; + +export const thresholdLine = (): ExpressionFunctionDefinition< + 'thresholdline', + Datatable | null, + ThresholdLine, + ExpressionValueThresholdLine +> => ({ + name: 'thresholdline', + help: i18n.translate('visTypeXy.function.thresholdLine.help', { + defaultMessage: 'Generates threshold line object', + }), + type: 'threshold_line', + args: { + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.thresholdLine.show.help', { + defaultMessage: 'Show threshould line', + }), + required: true, + }, + value: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.thresholdLine.value.help', { + defaultMessage: 'Threshold value', + }), + required: true, + }, + width: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.thresholdLine.width.help', { + defaultMessage: 'Width of threshold line', + }), + required: true, + }, + style: { + types: ['string'], + help: i18n.translate('visTypeXy.function.thresholdLine.style.help', { + defaultMessage: 'Style of threshold line. Can be full, dashed or dot-dashed', + }), + required: true, + }, + color: { + types: ['string'], + help: i18n.translate('visTypeXy.function.thresholdLine.color.help', { + defaultMessage: 'Color of threshold line', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'threshold_line', + show: args.show, + value: args.value, + width: args.width, + style: args.style, + color: args.color, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/time_marker.ts b/src/plugins/vis_type_xy/public/expression_functions/time_marker.ts new file mode 100644 index 000000000000000..3d9f609292c0038 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/time_marker.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { TimeMarker } from '../types'; + +export type ExpressionValueTimeMarker = ExpressionValueBoxed< + 'time_marker', + { + time: string; + class?: string; + color?: string; + opacity?: number; + width?: number; + } +>; + +export const timeMarker = (): ExpressionFunctionDefinition< + 'timemarker', + Datatable | null, + TimeMarker, + ExpressionValueTimeMarker +> => ({ + name: 'timemarker', + help: i18n.translate('visTypeXy.function.timemarker.help', { + defaultMessage: 'Generates time marker object', + }), + type: 'time_marker', + args: { + time: { + types: ['string'], + help: i18n.translate('visTypeXy.function.timeMarker.time.help', { + defaultMessage: 'Exact Time', + }), + required: true, + }, + class: { + types: ['string'], + help: i18n.translate('visTypeXy.function.timeMarker.class.help', { + defaultMessage: 'Css class name', + }), + }, + color: { + types: ['string'], + help: i18n.translate('visTypeXy.function.timeMarker.color.help', { + defaultMessage: 'Color of time marker', + }), + }, + opacity: { + types: ['number'], + help: i18n.translate('visTypeXy.function.timeMarker.opacity.help', { + defaultMessage: 'Opacity of time marker', + }), + }, + width: { + types: ['number'], + help: i18n.translate('visTypeXy.function.timeMarker.width.help', { + defaultMessage: 'Width of time marker', + }), + }, + }, + fn: (context, args) => { + return { + type: 'time_marker', + time: args.time, + class: args.class, + color: args.color, + opacity: args.opacity, + width: args.width, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/value_axis.ts b/src/plugins/vis_type_xy/public/expression_functions/value_axis.ts new file mode 100644 index 000000000000000..510ec9bc605d230 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/value_axis.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionValueCategoryAxis } from './category_axis'; +import type { CategoryAxis } from '../types'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; + +interface Arguments { + name: string; + axisParams: ExpressionValueCategoryAxis; +} + +export type ExpressionValueValueAxis = ExpressionValueBoxed< + 'value_axis', + { + name: string; + id: string; + show: boolean; + position: CategoryAxis['position']; + axisType: CategoryAxis['type']; + title: { + text?: string; + }; + labels: CategoryAxis['labels']; + scale: CategoryAxis['scale']; + } +>; + +export const valueAxis = (): ExpressionFunctionDefinition< + 'valueaxis', + Datatable | null, + Arguments, + ExpressionValueValueAxis +> => ({ + name: 'valueaxis', + help: i18n.translate('visTypeXy.function.valueaxis.help', { + defaultMessage: 'Generates value axis object', + }), + type: 'value_axis', + args: { + name: { + types: ['string'], + help: i18n.translate('visTypeXy.function.valueAxis.name.help', { + defaultMessage: 'Name of value axis', + }), + required: true, + }, + axisParams: { + types: ['category_axis'], + help: i18n.translate('visTypeXy.function.valueAxis.axisParams.help', { + defaultMessage: 'Value axis params', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'value_axis', + name: args.name, + id: args.axisParams.id, + show: args.axisParams.show, + position: args.axisParams.position, + axisType: args.axisParams.axisType, + title: args.axisParams.title, + scale: args.axisParams.scale, + labels: args.axisParams.labels, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts b/src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts new file mode 100644 index 000000000000000..fadf3d80a6e81a6 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { Scale } from '../types'; + +export type ExpressionValueScale = ExpressionValueBoxed< + 'vis_scale', + { + boundsMargin?: Scale['boundsMargin']; + defaultYExtents?: Scale['defaultYExtents']; + max?: Scale['max']; + min?: Scale['min']; + mode?: Scale['mode']; + setYExtents?: Scale['setYExtents']; + scaleType: Scale['type']; + } +>; + +export const visScale = (): ExpressionFunctionDefinition< + 'visscale', + Datatable | null, + Scale, + ExpressionValueScale +> => ({ + name: 'visscale', + help: i18n.translate('visTypeXy.function.scale.help', { + defaultMessage: 'Generates scale object', + }), + type: 'vis_scale', + args: { + boundsMargin: { + types: ['number', 'string'], + help: i18n.translate('visTypeXy.function.scale.boundsMargin.help', { + defaultMessage: 'Margin of bounds', + }), + }, + defaultYExtents: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.scale.defaultYExtents.help', { + defaultMessage: 'Flag which allows to scale to data bounds', + }), + }, + setYExtents: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.scale.setYExtents.help', { + defaultMessage: 'Flag which allows to set your own extents', + }), + }, + max: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.scale.max.help', { + defaultMessage: 'Max value', + }), + }, + min: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.scale.min.help', { + defaultMessage: 'Min value', + }), + }, + mode: { + types: ['string'], + help: i18n.translate('visTypeXy.function.scale.mode.help', { + defaultMessage: 'Scale mode. Can be normal, percentage, wiggle or silhouette', + }), + }, + type: { + types: ['string'], + help: i18n.translate('visTypeXy.function.scale.type.help', { + defaultMessage: 'Scale type. Can be linear, log or square root', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'vis_scale', + boundsMargin: args.boundsMargin, + defaultYExtents: args.defaultYExtents, + setYExtents: args.setYExtents, + max: args.max, + min: args.min, + mode: args.mode, + scaleType: args.type, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts b/src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts new file mode 100644 index 000000000000000..ecbc3640c035b15 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionValueVisDimension } from '../../../visualizations/public'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { Dimension } from '../types'; + +interface Arguments { + visDimension: ExpressionValueVisDimension; + params: string; + aggType: string; + label: string; +} + +export type ExpressionValueXYDimension = ExpressionValueBoxed< + 'xy_dimension', + { + label: string; + aggType: string; + params: Dimension['params']; + accessor: number; + format: Dimension['format']; + } +>; + +export const xyDimension = (): ExpressionFunctionDefinition< + 'xydimension', + Datatable | null, + Arguments, + ExpressionValueXYDimension +> => ({ + name: 'xydimension', + help: i18n.translate('visTypeXy.function.xydimension.help', { + defaultMessage: 'Generates xy dimension object', + }), + type: 'xy_dimension', + args: { + visDimension: { + types: ['vis_dimension'], + help: i18n.translate('visTypeXy.function.xyDimension.visDimension.help', { + defaultMessage: 'Dimension object config', + }), + required: true, + }, + label: { + types: ['string'], + help: i18n.translate('visTypeXy.function.xyDimension.label.help', { + defaultMessage: 'Label', + }), + }, + aggType: { + types: ['string'], + help: i18n.translate('visTypeXy.function.xyDimension.aggType.help', { + defaultMessage: 'Aggregation type', + }), + }, + params: { + types: ['string'], + default: '"{}"', + help: i18n.translate('visTypeXy.function.xyDimension.params.help', { + defaultMessage: 'Params', + }), + }, + }, + fn: (context, args) => { + return { + type: 'xy_dimension', + label: args.label, + aggType: args.aggType, + params: JSON.parse(args.params!), + accessor: args.visDimension.accessor as number, + format: args.visDimension.format, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts new file mode 100644 index 000000000000000..b8b8c0e8b8cca82 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import type { ExpressionFunctionDefinition, Datatable, Render } from '../../../expressions/public'; +import type { ChartType } from '../../common'; +import type { VisParams, XYVisConfig } from '../types'; + +export const visName = 'xy_vis'; +export interface RenderValue { + visData: Datatable; + visType: ChartType; + visConfig: VisParams; + syncColors: boolean; +} + +export type VisTypeXyExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof visName, + Datatable, + XYVisConfig, + Render +>; + +export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ + name: visName, + type: 'render', + context: { + types: ['datatable'], + }, + help: i18n.translate('visTypeXy.functions.help', { + defaultMessage: 'XY visualization', + }), + args: { + type: { + types: ['string'], + default: '""', + help: 'xy vis type', + }, + chartType: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.args.chartType.help', { + defaultMessage: 'Type of a chart. Can be line, area or histogram', + }), + }, + addTimeMarker: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.addTimeMarker.help', { + defaultMessage: 'Show time marker', + }), + }, + addLegend: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.addLegend.help', { + defaultMessage: 'Show chart legend', + }), + }, + addTooltip: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.addTooltip.help', { + defaultMessage: 'Show tooltip on hover', + }), + }, + legendPosition: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.legendPosition.help', { + defaultMessage: 'Position the legend on top, bottom, left, right of the chart', + }), + }, + categoryAxes: { + types: ['category_axis'], + help: i18n.translate('visTypeXy.function.args.categoryAxes.help', { + defaultMessage: 'Category axis config', + }), + multi: true, + }, + thresholdLine: { + types: ['threshold_line'], + help: i18n.translate('visTypeXy.function.args.thresholdLine.help', { + defaultMessage: 'Threshold line config', + }), + }, + labels: { + types: ['label'], + help: i18n.translate('visTypeXy.function.args.labels.help', { + defaultMessage: 'Chart labels config', + }), + }, + orderBucketsBySum: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.orderBucketsBySum.help', { + defaultMessage: 'Order buckets by sum', + }), + }, + seriesParams: { + types: ['series_param'], + help: i18n.translate('visTypeXy.function.args.seriesParams.help', { + defaultMessage: 'Series param config', + }), + multi: true, + }, + valueAxes: { + types: ['value_axis'], + help: i18n.translate('visTypeXy.function.args.valueAxes.help', { + defaultMessage: 'Value axis config', + }), + multi: true, + }, + radiusRatio: { + types: ['number'], + help: i18n.translate('visTypeXy.function.args.radiusRatio.help', { + defaultMessage: 'Dot size ratio', + }), + }, + gridCategoryLines: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.gridCategoryLines.help', { + defaultMessage: 'Show grid category lines in chart', + }), + }, + gridValueAxis: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.gridValueAxis.help', { + defaultMessage: 'Name of value axis for which we show grid', + }), + }, + isVislibVis: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.isVislibVis.help', { + defaultMessage: + 'Flag to indicate old vislib visualizations. Used for backwards compatibility including colors', + }), + }, + detailedTooltip: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.detailedTooltip.help', { + defaultMessage: 'Show detailed tooltip', + }), + }, + fittingFunction: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.fittingFunction.help', { + defaultMessage: 'Name of fitting function', + }), + }, + times: { + types: ['time_marker'], + help: i18n.translate('visTypeXy.function.args.times.help', { + defaultMessage: 'Time marker config', + }), + multi: true, + }, + palette: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.palette.help', { + defaultMessage: 'Defines the chart palette name', + }), + }, + xDimension: { + types: ['xy_dimension', 'null'], + help: i18n.translate('visTypeXy.function.args.xDimension.help', { + defaultMessage: 'X axis dimension config', + }), + }, + yDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.yDimension.help', { + defaultMessage: 'Y axis dimension config', + }), + multi: true, + }, + zDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.zDimension.help', { + defaultMessage: 'Z axis dimension config', + }), + multi: true, + }, + widthDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.widthDimension.help', { + defaultMessage: 'Width dimension config', + }), + multi: true, + }, + seriesDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.seriesDimension.help', { + defaultMessage: 'Series dimension config', + }), + multi: true, + }, + splitRowDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.splitRowDimension.help', { + defaultMessage: 'Split by row dimension config', + }), + multi: true, + }, + splitColumnDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.splitColumnDimension.help', { + defaultMessage: 'Split by column dimension config', + }), + multi: true, + }, + }, + fn(context, args, handlers) { + const visType = args.chartType; + const visConfig = { + type: args.chartType, + addLegend: args.addLegend, + addTooltip: args.addTooltip, + legendPosition: args.legendPosition, + addTimeMarker: args.addTimeMarker, + categoryAxes: args.categoryAxes.map((categoryAxis) => ({ + ...categoryAxis, + type: categoryAxis.axisType, + })), + orderBucketsBySum: args.orderBucketsBySum, + labels: args.labels, + thresholdLine: args.thresholdLine, + valueAxes: args.valueAxes.map((valueAxis) => ({ ...valueAxis, type: valueAxis.axisType })), + grid: { + categoryLines: args.gridCategoryLines, + valueAxis: args.gridValueAxis, + }, + seriesParams: args.seriesParams.map((seriesParam) => ({ + ...seriesParam, + type: seriesParam.seriesParamType, + })), + radiusRatio: args.radiusRatio, + times: args.times, + isVislibVis: args.isVislibVis, + detailedTooltip: args.detailedTooltip, + palette: { + type: 'palette', + name: args.palette, + }, + fittingFunction: args.fittingFunction, + dimensions: { + x: args.xDimension, + y: args.yDimension, + z: args.zDimension, + width: args.widthDimension, + series: args.seriesDimension, + splitRow: args.splitRowDimension, + splitColumn: args.splitColumnDimension, + }, + } as VisParams; + + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', context); + } + + return { + type: 'render', + as: visName, + value: { + context, + visType, + visConfig, + visData: context, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + }, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index d414da8f6dc9788..7bdb4f78bc631d0 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -12,8 +12,6 @@ import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/p import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; - -import { createVisTypeXyVisFn } from './xy_vis_fn'; import { setDataActions, setFormatService, @@ -23,10 +21,13 @@ import { setPalettesService, setTrackUiMetric, } from './services'; + import { visTypesDefinitions } from './vis_types'; import { LEGACY_CHARTS_LIBRARY } from '../common'; import { xyVisRenderer } from './vis_renderer'; +import * as expressionFunctions from './expression_functions'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface VisTypeXyPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -66,8 +67,18 @@ export class VisTypeXyPlugin setUISettings(core.uiSettings); setThemeService(charts.theme); setPalettesService(charts.palettes); - [createVisTypeXyVisFn].forEach(expressions.registerFunction); + expressions.registerRenderer(xyVisRenderer); + expressions.registerFunction(expressionFunctions.visTypeXyVisFn); + expressions.registerFunction(expressionFunctions.categoryAxis); + expressions.registerFunction(expressionFunctions.timeMarker); + expressions.registerFunction(expressionFunctions.valueAxis); + expressions.registerFunction(expressionFunctions.seriesParam); + expressions.registerFunction(expressionFunctions.thresholdLine); + expressions.registerFunction(expressionFunctions.label); + expressions.registerFunction(expressionFunctions.visScale); + expressions.registerFunction(expressionFunctions.xyDimension); + visTypesDefinitions.forEach(visualizations.createBaseVisualization); } diff --git a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts index c425eb71117e85e..e15f9c420770200 100644 --- a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts @@ -1414,6 +1414,9 @@ export const sampleAreaVis = { color: '#E7664C', }, labels: {}, + palette: { + name: 'default', + }, }, }, editorConfig: { @@ -1575,6 +1578,9 @@ export const sampleAreaVis = { style: 'full', color: '#E7664C', }, + palette: { + name: 'default', + }, labels: {}, dimensions: { x: { diff --git a/src/plugins/vis_type_xy/public/to_ast.test.ts b/src/plugins/vis_type_xy/public/to_ast.test.ts index 22e2d5f1cd9cc82..4437986eff5f745 100644 --- a/src/plugins/vis_type_xy/public/to_ast.test.ts +++ b/src/plugins/vis_type_xy/public/to_ast.test.ts @@ -42,7 +42,7 @@ describe('xy vis toExpressionAst function', () => { it('should match basic snapshot', () => { toExpressionAst(vis, params); - const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0]; expect(builtExpression).toMatchSnapshot(); }); diff --git a/src/plugins/vis_type_xy/public/to_ast.ts b/src/plugins/vis_type_xy/public/to_ast.ts index 84331af3a532953..c0a0ee566a4453f 100644 --- a/src/plugins/vis_type_xy/public/to_ast.ts +++ b/src/plugins/vis_type_xy/public/to_ast.ts @@ -11,13 +11,122 @@ import moment from 'moment'; import { VisToExpressionAst, getVisSchemas } from '../../visualizations/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { BUCKET_TYPES } from '../../data/public'; +import { Labels } from '../../charts/public'; -import { DateHistogramParams, Dimensions, HistogramParams, VisParams } from './types'; -import { visName, VisTypeXyExpressionFunctionDefinition } from './xy_vis_fn'; +import { + DateHistogramParams, + Dimensions, + Dimension, + HistogramParams, + VisParams, + CategoryAxis, + SeriesParam, + ThresholdLine, + ValueAxis, + Scale, + TimeMarker, +} from './types'; +import { visName, VisTypeXyExpressionFunctionDefinition } from './expression_functions/xy_vis_fn'; import { XyVisType } from '../common'; import { getEsaggsFn } from './to_ast_esaggs'; import { TimeRangeBounds } from '../../data/common'; +const prepareLabel = (data: Labels) => { + const label = buildExpressionFunction('label', { + ...data, + }); + + return buildExpression([label]); +}; + +const prepareScale = (data: Scale) => { + const scale = buildExpressionFunction('visscale', { + ...data, + }); + + return buildExpression([scale]); +}; + +const prepareThresholdLine = (data: ThresholdLine) => { + const thresholdLine = buildExpressionFunction('thresholdline', { + ...data, + }); + + return buildExpression([thresholdLine]); +}; + +const prepareTimeMarker = (data: TimeMarker) => { + const timeMarker = buildExpressionFunction('timemarker', { + ...data, + }); + + return buildExpression([timeMarker]); +}; + +const prepareCategoryAxis = (data: CategoryAxis) => { + const categoryAxis = buildExpressionFunction('categoryaxis', { + id: data.id, + show: data.show, + position: data.position, + type: data.type, + title: data.title.text, + scale: prepareScale(data.scale), + labels: prepareLabel(data.labels), + }); + + return buildExpression([categoryAxis]); +}; + +const prepareValueAxis = (data: ValueAxis) => { + const categoryAxis = buildExpressionFunction('valueaxis', { + name: data.name, + axisParams: prepareCategoryAxis({ + ...data, + }), + }); + + return buildExpression([categoryAxis]); +}; + +const prepareSeriesParam = (data: SeriesParam) => { + const seriesParam = buildExpressionFunction('seriesparam', { + label: data.data.label, + id: data.data.id, + drawLinesBetweenPoints: data.drawLinesBetweenPoints, + interpolate: data.interpolate, + lineWidth: data.lineWidth, + mode: data.mode, + show: data.show, + showCircles: data.showCircles, + type: data.type, + valueAxis: data.valueAxis, + }); + + return buildExpression([seriesParam]); +}; + +const prepareVisDimension = (data: Dimension) => { + const visDimension = buildExpressionFunction('visdimension', { accessor: data.accessor }); + + if (data.format) { + visDimension.addArgument('format', data.format.id); + visDimension.addArgument('formatParams', JSON.stringify(data.format.params)); + } + + return buildExpression([visDimension]); +}; + +const prepareXYDimension = (data: Dimension) => { + const xyDimension = buildExpressionFunction('xydimension', { + params: JSON.stringify(data.params), + aggType: data.aggType, + label: data.label, + visDimension: prepareVisDimension(data), + }); + + return buildExpression([xyDimension]); +}; + export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const schemas = getVisSchemas(vis, params); const dimensions: Dimensions = { @@ -62,15 +171,13 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params } } - const visConfig = { ...vis.params }; - (dimensions.y || []).forEach((yDimension) => { const yAgg = responseAggs[yDimension.accessor]; - const seriesParam = (visConfig.seriesParams || []).find( + const seriesParam = (vis.params.seriesParams || []).find( (param: any) => param.data.id === yAgg.id ); if (seriesParam) { - const usedValueAxis = (visConfig.valueAxes || []).find( + const usedValueAxis = (vis.params.valueAxes || []).find( (valueAxis: any) => valueAxis.id === seriesParam.valueAxis ); if (usedValueAxis?.scale.mode === 'percentage') { @@ -79,11 +186,34 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params } }); - visConfig.dimensions = dimensions; - const visTypeXy = buildExpressionFunction(visName, { type: vis.type.name as XyVisType, - visConfig: JSON.stringify(visConfig), + chartType: vis.params.type, + addTimeMarker: vis.params.addTimeMarker, + addLegend: vis.params.addLegend, + addTooltip: vis.params.addTooltip, + legendPosition: vis.params.legendPosition, + orderBucketsBySum: vis.params.orderBucketsBySum, + categoryAxes: vis.params.categoryAxes.map(prepareCategoryAxis), + valueAxes: vis.params.valueAxes.map(prepareValueAxis), + seriesParams: vis.params.seriesParams.map(prepareSeriesParam), + labels: prepareLabel(vis.params.labels), + thresholdLine: prepareThresholdLine(vis.params.thresholdLine), + gridCategoryLines: vis.params.grid.categoryLines, + gridValueAxis: vis.params.grid.valueAxis, + radiusRatio: vis.params.radiusRatio, + isVislibVis: vis.params.isVislibVis, + detailedTooltip: vis.params.detailedTooltip, + fittingFunction: vis.params.fittingFunction, + times: vis.params.times.map(prepareTimeMarker), + palette: vis.params.palette.name, + xDimension: dimensions.x ? prepareXYDimension(dimensions.x) : null, + yDimension: dimensions.y.map(prepareXYDimension), + zDimension: dimensions.z?.map(prepareXYDimension), + widthDimension: dimensions.width?.map(prepareXYDimension), + seriesDimension: dimensions.series?.map(prepareXYDimension), + splitRowDimension: dimensions.splitRow?.map(prepareXYDimension), + splitColumnDimension: dimensions.splitColumn?.map(prepareXYDimension), }); const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts index d5c5bfe004191ba..f025a36a82410aa 100644 --- a/src/plugins/vis_type_xy/public/types/config.ts +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -20,7 +20,7 @@ import { YDomainRange, } from '@elastic/charts'; -import { Dimension, Scale, ThresholdLine } from './param'; +import type { Dimension, Scale, ThresholdLine } from './param'; export interface Column { id: string | null; diff --git a/src/plugins/vis_type_xy/public/types/constants.ts b/src/plugins/vis_type_xy/public/types/constants.ts index 5c2f23b76aa9611..05ed0783d4c6869 100644 --- a/src/plugins/vis_type_xy/public/types/constants.ts +++ b/src/plugins/vis_type_xy/public/types/constants.ts @@ -6,52 +6,43 @@ * Side Public License, v 1. */ -import { $Values } from '@kbn/utility-types'; - -export const ChartMode = Object.freeze({ - Normal: 'normal' as const, - Stacked: 'stacked' as const, -}); -export type ChartMode = $Values; - -export const InterpolationMode = Object.freeze({ - Linear: 'linear' as const, - Cardinal: 'cardinal' as const, - StepAfter: 'step-after' as const, -}); -export type InterpolationMode = $Values; - -export const AxisType = Object.freeze({ - Category: 'category' as const, - Value: 'value' as const, -}); -export type AxisType = $Values; - -export const ScaleType = Object.freeze({ - Linear: 'linear' as const, - Log: 'log' as const, - SquareRoot: 'square root' as const, -}); -export type ScaleType = $Values; - -export const AxisMode = Object.freeze({ - Normal: 'normal' as const, - Percentage: 'percentage' as const, - Wiggle: 'wiggle' as const, - Silhouette: 'silhouette' as const, -}); -export type AxisMode = $Values; - -export const ThresholdLineStyle = Object.freeze({ - Full: 'full' as const, - Dashed: 'dashed' as const, - DotDashed: 'dot-dashed' as const, -}); -export type ThresholdLineStyle = $Values; - -export const ColorMode = Object.freeze({ - Background: 'Background' as const, - Labels: 'Labels' as const, - None: 'None' as const, -}); -export type ColorMode = $Values; +export enum ChartMode { + Normal = 'normal', + Stacked = 'stacked', +} + +export enum InterpolationMode { + Linear = 'linear', + Cardinal = 'cardinal', + StepAfter = 'step-after', +} + +export enum AxisType { + Category = 'category', + Value = 'value', +} + +export enum ScaleType { + Linear = 'linear', + Log = 'log', + SquareRoot = 'square root', +} + +export enum AxisMode { + Normal = 'normal', + Percentage = 'percentage', + Wiggle = 'wiggle', + Silhouette = 'silhouette', +} + +export enum ThresholdLineStyle { + Full = 'full', + Dashed = 'dashed', + DotDashed = 'dot-dashed', +} + +export enum ColorMode { + Background = 'Background', + Labels = 'Labels', + None = 'None', +} diff --git a/src/plugins/vis_type_xy/public/types/index.ts b/src/plugins/vis_type_xy/public/types/index.ts index d612e9fcf5f6fd5..6abbdfabaa9563b 100644 --- a/src/plugins/vis_type_xy/public/types/index.ts +++ b/src/plugins/vis_type_xy/public/types/index.ts @@ -9,4 +9,4 @@ export * from './constants'; export * from './config'; export * from './param'; -export * from './vis_type'; +export type { VisTypeNames, XyVisTypeDefinition } from './vis_type'; diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts index 69b6daf077a3293..f90899620126aa0 100644 --- a/src/plugins/vis_type_xy/public/types/param.ts +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -6,13 +6,21 @@ * Side Public License, v 1. */ -import { Fit, Position } from '@elastic/charts'; - -import { Style, Labels, PaletteOutput } from '../../../charts/public'; -import { SchemaConfig } from '../../../visualizations/public'; - -import { ChartType } from '../../common'; -import { +import type { Fit, Position } from '@elastic/charts'; +import type { Style, Labels, PaletteOutput } from '../../../charts/public'; +import type { SchemaConfig } from '../../../visualizations/public'; +import type { ChartType, XyVisType } from '../../common'; +import type { + ExpressionValueCategoryAxis, + ExpressionValueSeriesParam, + ExpressionValueValueAxis, + ExpressionValueLabel, + ExpressionValueThresholdLine, + ExpressionValueTimeMarker, + ExpressionValueXYDimension, +} from '../expression_functions'; + +import type { ChartMode, AxisMode, AxisType, @@ -47,7 +55,7 @@ export interface CategoryAxis { * remove with vis_type_vislib * https://github.com/elastic/kibana/issues/56143 */ - style: Partial