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 51e8d1a0b6befc..fd46a8a0f82c12 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 @@ -21,6 +21,7 @@ readonly links: { readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; + readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; }; @@ -29,6 +30,10 @@ readonly links: { }; readonly metricbeat: { readonly base: string; + readonly configure: string; + readonly httpEndpoint: string; + readonly install: string; + readonly start: string; }; readonly enterpriseSearch: { readonly base: string; diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index a5b8010f21f97f..8e4695bfc66624 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -86,7 +86,7 @@ endif::[] [cols="2*<"] |=== -| [[ems-hostname]]`hostname` +| [[ems-host]]`host` | Specifies the host of the backend server. To allow remote users to connect, set the value to the IP address or DNS name of the {hosted-ems} container. *Default: _your-hostname_*. <>. | `port` @@ -199,7 +199,7 @@ TIP: The available basemaps and boundaries can be explored from the `/maps` endp [[elastic-maps-server-kibana]] ==== Kibana configuration -With {hosted-ems} running, add the `map.emsUrl` configuration key in your <> file pointing to the root of the service. This setting will point {kib} to request EMS basemaps and boundaries from {hosted-ems}. Typically this will be the URL to the <> of {hosted-ems}. For example, `map.emsUrl: https://my-ems-server:8080`. +With {hosted-ems} running, add the `map.emsUrl` configuration key in your <> file pointing to the root of the service. This setting will point {kib} to request EMS basemaps and boundaries from {hosted-ems}. Typically this will be the URL to the <> of {hosted-ems}. For example, `map.emsUrl: https://my-ems-server:8080`. [float] diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 3c66e187bf59cd..265bf6bfaea304 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -68,9 +68,9 @@ To enable a blended layer that dynamically shows clusters or documents: [role="xpack"] [[maps-top-hits-aggregation]] -=== Top hits per entity +=== Display the most relevant documents per entity -You can display the most relevant documents per entity, for example, the most recent GPS tracks per flight. +Use *Top hits per entity* to display the most relevant documents per entity, for example, the most recent GPS tracks per flight route. To get this data, {es} first groups your data using a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation], then accumulates the most relevant documents based on sort order for each entry using a {ref}/search-aggregations-metrics-top-hits-aggregation.html[top hits metric aggregation]. diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index 8edd2f93121681..6012ae394c832a 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -8,12 +8,8 @@ The Debian package for Kibana can be <> or from our <>. It can be used to install Kibana on any Debian-based system such as Debian and Ubuntu. -This package is free to use under the Elastic license. It contains open source -and free commercial features and access to paid commercial features. -<> to try out all of the -paid commercial features. See the -https://www.elastic.co/subscriptions[Subscriptions] page for information about -Elastic license levels. +This package contains both free and subscription features. +<> to try out all of the features. The latest stable version of Kibana can be found on the link:/downloads/kibana[Download Kibana] page. Other versions can diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index 01a9c5718f14bf..216ec849147b41 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -13,12 +13,8 @@ and Oracle Enterprise. NOTE: RPM install is not supported on distributions with old versions of RPM, such as SLES 11 and CentOS 5. Please see <> instead. -This package is free to use under the Elastic license. It contains open source -and free commercial features and access to paid commercial features. -<> to try out all of the -paid commercial features. See the -https://www.elastic.co/subscriptions[Subscriptions] page for information about -Elastic license levels. +This package contains both free and subscription features. +<> to try out all of the features. The latest stable version of Kibana can be found on the link:/downloads/kibana[Download Kibana] page. Other versions can diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index 8eef43f7961670..bb51d98a4f922b 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -7,12 +7,8 @@ Kibana is provided for Linux and Darwin as a `.tar.gz` package. These packages are the easiest formats to use when trying out Kibana. -These packages are free to use under the Elastic license. They contain open -source and free commercial features and access to paid commercial features. -<> to try out all of the -paid commercial features. See the -https://www.elastic.co/subscriptions[Subscriptions] page for information about -Elastic license levels. +This package contains both free and subscription features. +<> to try out all of the features. The latest stable version of Kibana can be found on the link:/downloads/kibana[Download Kibana] page. diff --git a/docs/setup/install/windows.asciidoc b/docs/setup/install/windows.asciidoc index 4a5a855e0bbcf2..b4204cc623f0f3 100644 --- a/docs/setup/install/windows.asciidoc +++ b/docs/setup/install/windows.asciidoc @@ -6,12 +6,8 @@ Kibana can be installed on Windows using the `.zip` package. -This package is free to use under the Elastic license. It contains open source -and free commercial features and access to paid commercial features. -<> to try out all of the -paid commercial features. See the -https://www.elastic.co/subscriptions[Subscriptions] page for information about -Elastic license levels. +This package contains both free and subscription features. +<> to try out all of the features. The latest stable version of Kibana can be found on the link:/downloads/kibana[Download Kibana] page. diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts index c70f95b9ddc110..3bb97e57ca0a3c 100644 --- a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts +++ b/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { createGunzip } from 'zlib'; import mockFs from 'mock-fs'; import { createReadStream } from 'fs'; @@ -54,6 +55,11 @@ describe('getPayloadSize', () => { const result = getResponsePayloadBytes(readStream); expect(result).toBe(Buffer.byteLength(data)); }); + + test('ignores streams that are not instances of ReadStream', async () => { + const result = getResponsePayloadBytes(createGunzip()); + expect(result).toBe(undefined); + }); }); describe('handles plain responses', () => { @@ -72,6 +78,11 @@ describe('getPayloadSize', () => { const result = getResponsePayloadBytes(payload); expect(result).toBe(JSON.stringify(payload).length); }); + + test('returns undefined when source is not plain object', () => { + const result = getResponsePayloadBytes([1, 2, 3]); + expect(result).toBe(undefined); + }); }); describe('handles content-length header', () => { diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.ts index de96ad70027315..c7aeb0e8cac2bb 100644 --- a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts +++ b/packages/kbn-legacy-logging/src/utils/get_payload_size.ts @@ -6,14 +6,13 @@ * Side Public License, v 1. */ -import type { ReadStream } from 'fs'; +import { isPlainObject } from 'lodash'; +import { ReadStream } from 'fs'; import type { ResponseObject } from '@hapi/hapi'; const isBuffer = (obj: unknown): obj is Buffer => Buffer.isBuffer(obj); -const isObject = (obj: unknown): obj is Record => - typeof obj === 'object' && obj !== null; const isFsReadStream = (obj: unknown): obj is ReadStream => - typeof obj === 'object' && obj !== null && 'bytesRead' in obj; + typeof obj === 'object' && obj !== null && 'bytesRead' in obj && obj instanceof ReadStream; const isString = (obj: unknown): obj is string => typeof obj === 'string'; /** @@ -56,7 +55,7 @@ export function getResponsePayloadBytes( return Buffer.byteLength(payload); } - if (isObject(payload)) { + if (isPlainObject(payload)) { return Buffer.byteLength(JSON.stringify(payload)); } diff --git a/rfcs/images/api_doc_pick.png b/rfcs/images/api_doc_pick.png new file mode 100644 index 00000000000000..825fa47b266cb9 Binary files /dev/null and b/rfcs/images/api_doc_pick.png differ diff --git a/rfcs/images/api_doc_tech.png b/rfcs/images/api_doc_tech.png new file mode 100644 index 00000000000000..8c06d4ef3ebe8f Binary files /dev/null and b/rfcs/images/api_doc_tech.png differ diff --git a/rfcs/images/api_doc_tech_compare.png b/rfcs/images/api_doc_tech_compare.png new file mode 100644 index 00000000000000..46388b2a09a507 Binary files /dev/null and b/rfcs/images/api_doc_tech_compare.png differ diff --git a/rfcs/images/api_docs.png b/rfcs/images/api_docs.png new file mode 100644 index 00000000000000..d7e2e517e64650 Binary files /dev/null and b/rfcs/images/api_docs.png differ diff --git a/rfcs/images/api_docs_package_current.png b/rfcs/images/api_docs_package_current.png new file mode 100644 index 00000000000000..1a8f26dfad446c Binary files /dev/null and b/rfcs/images/api_docs_package_current.png differ diff --git a/rfcs/images/api_info.png b/rfcs/images/api_info.png new file mode 100644 index 00000000000000..dc5ecc845cb722 Binary files /dev/null and b/rfcs/images/api_info.png differ diff --git a/rfcs/images/current_api_doc_links.png b/rfcs/images/current_api_doc_links.png new file mode 100644 index 00000000000000..e52a273cf24e38 Binary files /dev/null and b/rfcs/images/current_api_doc_links.png differ diff --git a/rfcs/images/new_api_docs_with_links.png b/rfcs/images/new_api_docs_with_links.png new file mode 100644 index 00000000000000..bfa514b9195338 Binary files /dev/null and b/rfcs/images/new_api_docs_with_links.png differ diff --git a/rfcs/images/repeat_primitive_signature.png b/rfcs/images/repeat_primitive_signature.png new file mode 100644 index 00000000000000..7c98eefbcf50df Binary files /dev/null and b/rfcs/images/repeat_primitive_signature.png differ diff --git a/rfcs/images/repeat_type_links.png b/rfcs/images/repeat_type_links.png new file mode 100644 index 00000000000000..bff54d90e9cae0 Binary files /dev/null and b/rfcs/images/repeat_type_links.png differ diff --git a/rfcs/text/0014_api_documentation.md b/rfcs/text/0014_api_documentation.md new file mode 100644 index 00000000000000..b70636c63aad37 --- /dev/null +++ b/rfcs/text/0014_api_documentation.md @@ -0,0 +1,442 @@ +- Start Date: 2020-12-21 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) +- [POC PR](https://github.com/elastic/kibana/pull/86232) + +# Goal + +Automatically generate API documentation for every plugin that exposes a public API within Kibana in order to help Kibana plugin developers +find and understand the services available to them. Automatic generation ensures the APIs are _always_ up to date. The system will make it easy to find +APIs that are lacking documentation. + +Note this does not cover REST API docs, but is targetted towards our javascript +plugin APIs. + +# Technology: ts-morph vs api-extractor + +[Api-extractor](https://api-extractor.com/) is a utility built from microsoft that parses typescript code into json files that can then be used in a custom [api-documenter](https://api-extractor.com/pages/setup/generating_docs/) in order to build documentation. This is what we [have now](https://github.com/elastic/kibana/tree/master/docs/development), except we use the default api-documenter. + +## Limitations with the current implementation using api-extractor & api-documenter + +The current implementation relies on the default api-documenter. It has the following limitations: + +- One page per API item +- Files are .md not .mdx +- There is no entry page per plugin (just an index.md per plugin/public and plugin/server) +- Incorrectly marks these entries as packages. + +![image](../images/api_docs_package_current.png) + +- Does not generate links to APIs exposed from other plugins, nor inside the same plugin. + +![image](../images/current_api_doc_links.png) + +## Options to improve + +We have two options to improve on the current implementation. We can use a custom api-documenter, or use ts-morph. + +### Custom Api-Documenter + +- According to the current maintainer of the sample api-documenter, it's a surprising amount of work to maintain. +- If we wish to re-use code from the sample api-documenter, we'll have to fork the rush-stack repo, or copy their code into our system. +- No verified ability to support cross plugin links. We do have some ideas (can explore creating a package.json for every page, and/or adding source file information to every node). +- More limited feature set, we wouldn't get thinks like references and source file paths. +- There are very few examples of other companies using custom api-documenters to drive their documentation systems (I could not find any on github). + +### Custom implementation using ts-morph + +[ts-morph](https://github.com/dsherret/ts-morph) is a utility built and maintained by a single person, which sits a layer above the raw typescript compiler. + +- Requires manually converting the types to how we want them to be displayed in the UI. Certain types have to be handled specially to show up +in the right way (for example, for arrow functions to be categorized as functions). This special handling is the bulk of the logic in the PR, and +may be a maintenance burden. +- Relies on a package maintained by a single person, albiet they have been very responsive and have a history of keeping the library up to date with +typescript upgrades. +- Affords us flexibility to do things like extract the setup and start types, grab source file paths to create links to github, and get +reference counts (reference counts not implemented in MVP). +- There are some issues with type links and signatures not working correctly (see https://github.com/dsherret/ts-morph/issues/923). + +![image](../images/new_api_docs_with_links.png) + +## Recommendation: ts-morph for the short term, switch to api-extractor when limitations can be worked around + +Both approaches will have a decent amount of code to maintain, but the api-extractor approach appears to be a more stable long term solution, since it's built and maintained by Microsoft and +is likely going to grow in popularity as more TypeScript API doc systems exist. +If we had a working example that supported cross plugin links, I would suggest continuing down that road. However, we don't, while we _do_ have a working ts-morph implementation. + +I recommend that we move ahead with ts-morph in the short term, because we have an implementation that offers a much improved experience over the current system, but that we continually +re-evaluate as time goes on and we learn more about the maintenance burden of the current approach, and see what happens with our priorities and the api-extractor library. + +Progress over perfection. + +![image](../images/api_doc_tech_compare.png) + +If we do switch, we can re-use all of the tests that take example TypeScript files and verify the resulting ApiDeclaration shapes. + +# Terminology + +**API** - A plugin's public API consists of every function, class, interface, type, variable, etc, that is exported from it's index.ts file, or returned from it's start or setup +contract. + +**API Declaration** - Each function, class, interface, type, variable, etc, that is part of a plugins public API is a "declaration". This +terminology is motivated by [these docs](https://www.typescriptlang.org/docs/handbook/modules.html#exporting-a-declaration). + +# MVP + +Every plugin will have one or more API reference pages. Every exported declaration will be listed in the page. It is first split by "scope" - client, server and common. Underneath +that, setup and start contracts are at the top, the remaining declarations are grouped by type (classes, functions, interfaces, etc). +Plugins may opt to have their API split into "service" sections (see [proposed manifest file changes](#manifest-file-changes)). If a plugin uses service folders, the API doc system will automatically group declarations that are defined inside the service folder name. This is a simple way to break down very large plugins. The start and setup contract will +always remain with the main plugin name. + +![image](../images/api_docs.png) + +- Cross plugin API links work inside `signature`. +- Github links with source file and line number +- using `serviceFolders` to split large plugins + +## Post MVP + +- Plugin `{@link AnApi}` links work. Will need to decide if we only support per plugin links, or if we should support a way to do this across plugins. +- Ingesting stats like number of public APIs, and number of those missing comments +- Include and expose API references +- Use namespaces to split large plugins + +# Information available for each API declaration + +We have the following pieces of information available from each declaration: + +- Label. The name of the function, class, interface, etc. + +- Description. Any comment that was able to be extracted. Currently it's not possible for this data to be formatted, for example if it has a code example with back tics. This +is dependent on the elastic-docs team moving the infrastructure to NextJS instead of Gatsby, but it will eventually be supported. + +- Tags. Any `@blahblah` tags that were extracted from comments. Known tags, like `beta`, will be show help text in a tooltip when hovered over. + +- Type. This can be thought of as the _kind_ of type (see [TypeKind](#typekind)). It allows us to group each type into a category. It can be a primitive, or a +more complex grouping. Possibilities are: array, string, number, boolean, object, class, interface, function, compound (unions or intersections) + +- Required or optional. (whether or not the type was written with `| undefined` or `?`). This terminology makes the most sense for function +parameters, not as much when thinking about an exported variable that might be undefined. + +- Signature. This is only relevant for some types: functions, objects, type, arrays and compound. Classes and interfaces would be too large. +For primitives, this is equivalent to "type". + +- Children. Only relevant for some types, this would include parameters for functions, class members and functions for classes, properties for +interfaces and objects. This makes the structure recursive. Each child is a nested API component. + +- Return comment. Only relevant for function types. + +![image](../images/api_info.png) + + +### ApiDeclaration type + +```ts +interface ApiDeclaration { + label: string; + type: TypeKind; // string, number, boolean, class, interface, function, type, etc. + description: TextWithLinks; + signature: TextWithLinks; + tags: string[]; // Declarations may be tagged as beta, or deprecated. + children: ApiDeclaration[]; // Recursive - this could be function parameters, class members, or interface/object properties. + returnComment?: TextWithLinks; + lifecycle?: Lifecycle.START | Lifecycle.SETUP; +} + +``` + +# Architecture design + +## Location + +The generated docs will reside inside the kibana repo, inside a top level `api_docs` folder. In the long term, we could investigate having the docs system run a script to generated the mdx files, so we don’t need to store them inside the repo. Every ci run should destroy and re-create this folder so removed plugins don't have lingering documentation files. + +They will be hosted online wherever the new docs system ends up. This can temporarily be accessed at https://elasticdocstest.netlify.app/docs/. + +## Algorithm overview + +The first stage is to collect the list of plugins using the existing `findPlugins` logic. + +For every plugin, the initial list of ts-morph api node declarations are collected from three "scope" files: + - plugin/public/index.ts + - plugin/server/index.ts + - plugin/common/index.ts + +Each ts-morph declaration is then transformed into an [ApiDeclaration](#ApiDeclaration-type) type, which is recursive due to the `children` property. Each +type of declaration is handled slightly differently, mainly in regard to whether or not a signature or return type is added, and how children are added. + +For example: + +```ts +if (node.isClassDeclaration()) { + // No signature or return. + return { + label, + description, + type: TypeKind.ClassKind, + // The class members are captured in the children array. + children: getApiDeclaration(node.getMembers()), + } +} else if (node.isFunctionDeclaration()) { + return { + label, + description, + signature: getSignature(node), + returnComment: getReturnComment(node), + type: TypeKind.FunctionKind, + // The function parameters are captured in the children array. This logic is more specific because + // the comments for a function parameter are captured in the function comment, with "@param" tags. + children: getParameterList(node.getParameters(), getParamTagComments(node)), + } +} if (...) +.... +``` + +The handling of each specific type is what encompasses the vast majority of the logic in the PR. + +The public and server scope have 0-2 special interfaces indicated by "lifecycle". This is determined by using ts-morph to extract the first two generic types +passed to `... extends Plugin` in the class defined inside the plugin's `plugin.ts` file. + +A [PluginApi](#pluginapi) is generated for each plugin, which is used to generate the json and mdx files. One or more json/mdx file pair + per plugin may be created, depending on the value of `serviceFolders` inside the plugin's manifest files. This is because some plugins have such huge APIs that + it is too large to render in a single page. + +![image](../images/api_doc_tech.png) + +## Types + +### TypeKind + +TypeKind is an enum that will identify what "category" or "group" name we can call this particular export. Is it a function, an interface, a class a variable, etc. +This list is likely incomplete, and we'll expand as needed. + +```ts +export enum TypeKind { + ClassKind = 'Class', + FunctionKind = 'Function', + ObjectKind = 'Object', + InterfaceKind = 'Interface', + TypeKind = 'Type', // For things like `export type Foo = ...` + UnknownKind = 'Unknown', // For the special "unknown" typescript type. + AnyKind = 'Any', // For the "any" kind, which should almost never be used in our public API. + UnCategorized = 'UnCategorized', // There are a lot of ts-morph types, if I encounter something not handled, I dump it in here. + StringKind = 'string', + NumberKind = 'number', + BooleanKind = 'boolean', + ArrayKind = 'Array', + CompoundTypeKind = 'CompoundType', // Unions & intersections, to handle things like `string | number`. +} +``` + + +### Text with reference links + +Signatures, descriptions and return comments may all contain links to other API declarations. This information needs to be serializable into json. This serializable type encompasses the information needed to build the DocLink components within these fields. The logic of building +the DocLink components currently resides inside the elastic-docs system. It's unclear if this will change. + +```ts +/** + * This is used for displaying code or comments that may contain reference links. For example, a function + * signature that is `(a: import("src/plugin_b").Bar) => void` will be parsed into the following Array: + * + * ```ts + * [ + * '(a: ', + * { docId: 'pluginB', section: 'Bar', text: 'Bar' }, + * ') => void' + * ] + * ``` + * + * This is then used to render text with nested DocLinks so it looks like this: + * + * `(a: => ) => void` + */ +export type TextWithLinks = Array; + +/** + * The information neccessary to build a DocLink. + */ +export interface Reference { + docId: string; + section: string; + text: string; +} +``` + +### ScopeApi + +Scope API is essentially just grouping an array of ApiDeclarations into different categories that makes building the mdx files from a +single json file easier. + +```ts +export interface ScopeApi { + setup?: ApiDeclaration; + start?: ApiDeclaration; + classes: ApiDeclaration[]; + functions: ApiDeclaration[]; + interfaces: ApiDeclaration[]; + objects: ApiDeclaration[]; + enums: ApiDeclaration[]; + misc: ApiDeclaration[]; + // We may add more here as we sit fit to pull out of `misc`. +} +``` + +With this structure, the mdx files end up looking like: + +``` +### Start + +### Functions + +### Interfaces + +``` + +### PluginApi + +A plugin API is the component that is serialized into the json file. It is broken into public, server and common components. `serviceFolders` is a way for the system to +write separate mdx files depending on where each declaration is defined. This is because certain plugins (and core) +are huge, and can't be rendered in a single page. + + +```ts +export interface PluginApi { + id: string; + serviceFolders?: readonly string[]; + client: ScopeApi; + server: ScopeApi; + common: ScopeApi; +} +``` + +## kibana.json Manifest file changes + +### Using a kibana.json file for core + +For the purpose of API infrastructure, core is treated like any other plugin. This means it has to specify serviceFolders section inside a manifest file to be split into sub folders. There are other ways to tackle this - like a hard coded array just for the core folder, but I kept the logic as similar to the other plugins as possible. + +### New parameters + +**serviceFolders?: string[]** + +Used by the system to group services into sub-pages. Some plugins, like data and core, have such huge APIs they are very slow to contain in a single page, and they are less consummable by solution developers. The addition of an optional list of services folders will cause the system to automatically create a separate page with every API that is defined within that folder. The caveat is that core will need to define a manifest file in order to define its service folders... + +**teamOwner: string** + +Team owner can be determined via github CODEOWNERS file, but we want to encourage single team ownership per plugin. Requiring a team owner string in the manifest file will help with this and will allow the API doc system to manually add a section to every page that has a link to the team owner. Additional ideas are teamSlackChannel or teamEmail for further contact. + +**summary: string** + + +A brief description of the plugin can then be displayed in the automatically generated API documentation. + +# Future features + +## Indexing stats + +Can we index statistics about our API as part of this system? For example, I'm dumping information about which api declarations are missing comments in the console. + +## Longer term approach to "plugin service folders" + +Using sub folders is a short term plan. A long term plan hasn't been established yet, but it should fit in with our folder structure hierarchy goals, along with +any support we have for sharing services among a related set of plugins, that are not exposed as part of the public API. +# Recommendations for writing comments + +## @link comments for the referenced type + +Core has a pattern of writing comments like this: + +```ts + /** {@link IUiSettingsClient} */ + uiSettings: IUiSettingsClient; +``` + +I don't see the value in this. In the IDE, I can click on the IUiSettingsClient type and get directed there, and in the API doc system, the +type will already be clickable. This ends up with a weird looking API: + +![image](../images/repeat_type_links.png) + +The plan is to make @link comments work like links, which means this is unneccessary information. + +I propose we avoid this kind of pattern. + +## Export every referenced type + +The docs system handles broken link warnings but to avoid breaking the ci, I suggest we turn this off initially. However, this will mean +we may miss situations where we are referencing a type that is not actually exported. This will cause a broken link in the docs +system + +For example if your index.ts file has: +```ts +export type foo: string | AnInterface; +``` + +and does not also export `AnInterface`, this will be a broken link in the docs system. + +Until we have better CI tools to catch these mistakes, developers will need to export every referenced type. + +## Avoid `Pick` pattern + +Connected to the above, if you use `Pick`, there are two problems. One is that it's difficult for a developer to see the functionality +available to them at a glance, since they would have to keep flipping from the interface definition to the properties that have been picked. + +The second potential problem is that you will have to export the referenced type, and in some situations, it's an internal type that isn't exported. + +![image](../images/api_doc_pick.png) + +# Open questions + +## Required attribute + +`isRequired` is an optional parameter that can be used to display a badge next to the API. +We can mark function parameters that do not use `?` or `| undefined` as required. Open questions: + +1. Are we okay with a badge showing for `required` rather than `optional` when marking a parameter as optional is extra work for a developer, and `required` is the default? + +2. Should we only mark function parameters as `required` or interface/class parameters? Essentially, should any declaration that is not nullable +have the `required` tag? + +## Signatures on primitive types + +1. Should we _always_ include a signature for variables and parameters, even if they are a repeat of the TypeKind? For example: + +![image](../images/repeat_primitive_signature.png) + +2. If no, should we include signatures when the only difference is `| undefined`? For function parameters this information is captured by +the absence of the `required` badge. Is this obvious? What about class members/interface props? + +## Out of scope + +### REST API + +This RFC does not cover REST API documentation, though it worth considering where +REST APIs registered by plugins should go in the docs. The docs team has a proposal for this but it is not inside the `Kibana Developer Docs` mission. + +### Package APIs + +Package APIs are not covered in this RFC. + +# Adoption strategy + +In order to generate useful API documentation, we need to approach this by two sides. + +1. Establish a habit of writing documentation. +2. Establish a habit of reading documentation. + +Currently what often happens is a developer asks another developer a question directly, and it is answered. Every time this happens, ask yourself if +there is a link you can share instead of a direct answer. If there isn't, file an issue for that documentation to be created. When we start responding +to questions with links, solution developers will naturally start to look in the documentation _first_, saving everyone time! + +The APIs WILL need to be well commented or they won't be useful. We can measure the amount of missing comments and set a goal of reducing this number. + +# External documentation system examples + +- [Microsoft .NET](https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualbasic?view=netcore-3.1) +- [Android](https://developer.android.com/reference/androidx/packages) + +# Architecure review + +The primary concern coming out of the architecture review was over the technology choice of ts-morph vs api-extractor, and the potential maintenance +burdern of using ts-morph. For the short term, we've decide tech leads will own this section of code, we'll consider it experimental and + focus on deriving value out of it. Once we are confident of the value, we can focus on stabilizing the implementation details. \ No newline at end of file diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 7fd62d6f02e96b..da35373f57322a 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -39,6 +39,7 @@ export class DocLinksService { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`, configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/configuring-howto-filebeat.html`, + elasticsearchModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-module-elasticsearch.html`, elasticsearchOutput: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/elasticsearch-output.html`, startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`, exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`, @@ -53,6 +54,10 @@ export class DocLinksService { }, metricbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`, + configure: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/configuring-howto-metricbeat.html`, + httpEndpoint: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/http-endpoint.html`, + install: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation-configuration.html`, + start: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`, }, heartbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/heartbeat/${DOC_LINK_VERSION}`, @@ -116,6 +121,7 @@ export class DocLinksService { addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, elasticsearch: { + mapping: `${ELASTICSEARCH_DOCS}mapping.html`, remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, @@ -193,8 +199,11 @@ export class DocLinksService { alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, + metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, + monitorLogstash: `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html`, + troubleshootKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitor-troubleshooting.html`, }, security: { apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, @@ -257,6 +266,7 @@ export interface DocLinksStart { readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; + readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; }; @@ -265,6 +275,10 @@ export interface DocLinksStart { }; readonly metricbeat: { readonly base: string; + readonly configure: string; + readonly httpEndpoint: string; + readonly install: string; + readonly start: string; }; readonly enterpriseSearch: { readonly base: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 37ebbcaa752af1..75ed9aa5f150f6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -474,6 +474,7 @@ export interface DocLinksStart { readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; + readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; }; @@ -482,6 +483,10 @@ export interface DocLinksStart { }; readonly metricbeat: { readonly base: string; + readonly configure: string; + readonly httpEndpoint: string; + readonly install: string; + readonly start: string; }; readonly enterpriseSearch: { readonly base: string; diff --git a/src/core/server/http/logging/get_payload_size.test.ts b/src/core/server/http/logging/get_payload_size.test.ts index a4ab8919e8b6da..dba5c7be30f3b0 100644 --- a/src/core/server/http/logging/get_payload_size.test.ts +++ b/src/core/server/http/logging/get_payload_size.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { createGunzip } from 'zlib'; import type { Request } from '@hapi/hapi'; import Boom from '@hapi/boom'; @@ -96,6 +97,18 @@ describe('getPayloadSize', () => { expect(result).toBe(Buffer.byteLength(data)); }); + + test('ignores streams that are not instances of ReadStream', async () => { + const result = getResponsePayloadBytes( + { + variety: 'stream', + source: createGunzip(), + } as Response, + logger + ); + + expect(result).toBe(undefined); + }); }); describe('handles plain responses', () => { @@ -132,6 +145,17 @@ describe('getPayloadSize', () => { ); expect(result).toBe(JSON.stringify(payload).length); }); + + test('returns undefined when source is not a plain object', () => { + const result = getResponsePayloadBytes( + { + variety: 'plain', + source: [1, 2, 3], + } as Response, + logger + ); + expect(result).toBe(undefined); + }); }); describe('handles content-length header', () => { diff --git a/src/core/server/http/logging/get_payload_size.ts b/src/core/server/http/logging/get_payload_size.ts index 6dcaf3653d8420..8e6dea13e1fa17 100644 --- a/src/core/server/http/logging/get_payload_size.ts +++ b/src/core/server/http/logging/get_payload_size.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import type { ReadStream } from 'fs'; +import { isPlainObject } from 'lodash'; +import { ReadStream } from 'fs'; import { isBoom } from '@hapi/boom'; import type { Request } from '@hapi/hapi'; import { Logger } from '../../logging'; @@ -17,8 +18,15 @@ const isBuffer = (src: unknown, res: Response): src is Buffer => { return !isBoom(res) && res.variety === 'buffer' && res.source === src; }; const isFsReadStream = (src: unknown, res: Response): src is ReadStream => { - return !isBoom(res) && res.variety === 'stream' && res.source === src; + return ( + !isBoom(res) && + res.variety === 'stream' && + res.source === src && + res.source instanceof ReadStream + ); }; +const isString = (src: unknown, res: Response): src is string => + !isBoom(res) && res.variety === 'plain' && typeof src === 'string'; /** * Attempts to determine the size (in bytes) of a Hapi response @@ -57,10 +65,12 @@ export function getResponsePayloadBytes(response: Response, log: Logger): number return response.source.bytesRead; } - if (response.variety === 'plain') { - return typeof response.source === 'string' - ? Buffer.byteLength(response.source) - : Buffer.byteLength(JSON.stringify(response.source)); + if (isString(response.source, response)) { + return Buffer.byteLength(response.source); + } + + if (response.variety === 'plain' && isPlainObject(response.source)) { + return Buffer.byteLength(JSON.stringify(response.source)); } } catch (e) { // We intentionally swallow any errors as this information is diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 13ff8b14d9b432..b22bb6dc713422 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -56,7 +56,6 @@ import { SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern'; -import { getTopNavLinks } from '../components/top_nav/get_top_nav_links'; import { updateSearchSource } from '../helpers/update_search_source'; import { calcFieldCounts } from '../helpers/calc_field_counts'; import { getDefaultSort } from './doc_table/lib/get_default_sort'; @@ -198,7 +197,7 @@ function discoverController($route, $scope, Promise) { session: data.search.session, }); - const state = getState({ + const stateContainer = getState({ getStateDefaults, storeInSessionStorage: config.get('state:storeInSessionStorage'), history, @@ -213,7 +212,7 @@ function discoverController($route, $scope, Promise) { replaceUrlAppState, kbnUrlStateStorage, getPreviousAppState, - } = state; + } = stateContainer; if (appStateContainer.getState().index !== $scope.indexPattern.id) { //used index pattern is different than the given by url/state which is invalid @@ -323,10 +322,24 @@ function discoverController($route, $scope, Promise) { ) ); - const inspectorAdapters = { - requests: new RequestAdapter(), + $scope.opts = { + // number of records to fetch, then paginate through + sampleSize: config.get(SAMPLE_SIZE_SETTING), + timefield: getTimeField(), + savedSearch: savedSearch, + indexPatternList: $route.current.locals.savedObjects.ip.list, + config: config, + setHeaderActionMenu: getHeaderActionMenuMounter(), + filterManager, + setAppState, + data, + stateContainer, }; + const inspectorAdapters = ($scope.opts.inspectorAdapters = { + requests: new RequestAdapter(), + }); + $scope.timefilterUpdateHandler = (ranges) => { timefilter.setTime({ from: moment(ranges.from).toISOString(), @@ -358,7 +371,7 @@ function discoverController($route, $scope, Promise) { unlistenHistoryBasePath(); }); - const getFieldCounts = async () => { + $scope.opts.getFieldCounts = async () => { // the field counts aren't set until we have the data back, // so we wait for the fetch to be done before proceeding if ($scope.fetchStatus === fetchStatuses.COMPLETE) { @@ -374,20 +387,11 @@ function discoverController($route, $scope, Promise) { }); }); }; - - $scope.topNavMenu = getTopNavLinks({ - getFieldCounts, - indexPattern: $scope.indexPattern, - inspectorAdapters, - navigateTo: (path) => { - $scope.$evalAsync(() => { - history.push(path); - }); - }, - savedSearch, - services, - state, - }); + $scope.opts.navigateTo = (path) => { + $scope.$evalAsync(() => { + history.push(path); + }); + }; $scope.searchSource .setField('index', $scope.indexPattern) @@ -446,19 +450,6 @@ function discoverController($route, $scope, Promise) { $scope.state.index = $scope.indexPattern.id; $scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern); - $scope.opts = { - // number of records to fetch, then paginate through - sampleSize: config.get(SAMPLE_SIZE_SETTING), - timefield: getTimeField(), - savedSearch: savedSearch, - indexPatternList: $route.current.locals.savedObjects.ip.list, - config: config, - setHeaderActionMenu: getHeaderActionMenuMounter(), - filterManager, - setAppState, - data, - }; - const shouldSearchOnPageLoad = () => { // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index 720b79f53a5519..bb0014f4278a11 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -9,11 +9,8 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; import { Discover } from './discover'; -import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { esHits } from '../../__mocks__/es_hits'; import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { getTopNavLinks } from './top_nav/get_top_nav_links'; -import { DiscoverServices } from '../../build_services'; import { GetStateReturn } from '../angular/discover_state'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; @@ -25,6 +22,8 @@ import { SavedObject } from '../../../../../core/types'; import { navigationPluginMock } from '../../../../navigation/public/mocks'; import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; import { calcFieldCounts } from '../helpers/calc_field_counts'; +import { DiscoverProps } from './types'; +import { RequestAdapter } from '../../../../inspector/common'; const mockNavigation = navigationPluginMock.createStartContract(); @@ -45,17 +44,9 @@ jest.mock('../../kibana_services', () => { }; }); -function getProps(indexPattern: IndexPattern) { +function getProps(indexPattern: IndexPattern): DiscoverProps { const searchSourceMock = createSearchSourceMock({}); const state = ({} as unknown) as GetStateReturn; - const services = ({ - capabilities: { - discover: { - save: true, - }, - }, - uiSettings: mockUiSettings, - } as unknown) as DiscoverServices; return { fetch: jest.fn(), @@ -76,32 +67,25 @@ function getProps(indexPattern: IndexPattern) { opts: { config: mockUiSettings, data: dataPluginMock.createStartContract(), - fixedScroll: jest.fn(), filterManager: createFilterManagerMock(), + getFieldCounts: jest.fn(), indexPatternList: (indexPattern as unknown) as Array>, + inspectorAdapters: { requests: {} as RequestAdapter }, + navigateTo: jest.fn(), sampleSize: 10, savedSearch: savedSearchMock, + setAppState: jest.fn(), setHeaderActionMenu: jest.fn(), + stateContainer: state, timefield: indexPattern.timeFieldName || '', - setAppState: jest.fn(), }, resetQuery: jest.fn(), resultState: 'ready', rows: esHits, searchSource: searchSourceMock, setIndexPattern: jest.fn(), - showSaveQuery: true, state: { columns: [] }, timefilterUpdateHandler: jest.fn(), - topNavMenu: getTopNavLinks({ - getFieldCounts: jest.fn(), - indexPattern, - inspectorAdapters: inspectorPluginMock, - navigateTo: jest.fn(), - savedSearch: savedSearchMock, - services, - state, - }), updateQuery: jest.fn(), updateSavedQueryId: jest.fn(), }; diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index e6c4524f81f564..baee0623f0b5ae 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -41,6 +41,8 @@ import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { getTopNavLinks } from './top_nav/get_top_nav_links'; const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( @@ -77,11 +79,11 @@ export function Discover({ state, timefilterUpdateHandler, timeRange, - topNavMenu, updateQuery, updateSavedQueryId, unmappedFieldsConfig, }: DiscoverProps) { + const [expandedDoc, setExpandedDoc] = useState(undefined); const scrollableDesktop = useRef(null); const collapseIcon = useRef(null); const isMobile = () => { @@ -91,7 +93,24 @@ export function Discover({ const [toggleOn, toggleChart] = useState(true); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = getServices(); + const services = useMemo(() => getServices(), []); + const topNavMenu = useMemo( + () => + getTopNavLinks({ + getFieldCounts: opts.getFieldCounts, + indexPattern, + inspectorAdapters: opts.inspectorAdapters, + navigateTo: opts.navigateTo, + savedSearch: opts.savedSearch, + services, + state: opts.stateContainer, + onOpenInspector: () => { + // prevent overlapping + setExpandedDoc(undefined); + }, + }), + [indexPattern, opts, services] + ); const { TopNavMenu } = services.navigation.ui; const { trackUiMetric } = services; const { savedSearch, indexPatternList, config } = opts; @@ -318,12 +337,14 @@ export function Discover({ void; /** * Grid display settings persisted in Elasticsearch (e.g. column width) */ @@ -121,6 +129,7 @@ export const DiscoverGrid = ({ ariaLabelledBy, columns, indexPattern, + expandedDoc, onAddColumn, onFilter, onRemoveColumn, @@ -132,11 +141,11 @@ export const DiscoverGrid = ({ searchDescription, searchTitle, services, + setExpandedDoc, settings, showTimeCol, sort, }: DiscoverGridProps) => { - const [expanded, setExpanded] = useState(undefined); const defaultColumns = columns.includes('_source'); /** @@ -233,8 +242,8 @@ export const DiscoverGrid = ({ return ( )} - {expanded && ( + {expandedDoc && ( setExpanded(undefined)} + onClose={() => setExpandedDoc(undefined)} services={services} /> )} diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts index 7629495c85bb54..89cb60700074d1 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts @@ -29,6 +29,7 @@ test('getTopNavLinks result', () => { indexPattern: indexPatternMock, inspectorAdapters: inspectorPluginMock, navigateTo: jest.fn(), + onOpenInspector: jest.fn(), savedSearch: savedSearchMock, services, state, 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 0b23c31ac03c40..513508c478aa95 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 @@ -28,6 +28,7 @@ export const getTopNavLinks = ({ savedSearch, services, state, + onOpenInspector, }: { getFieldCounts: () => Promise>; indexPattern: IndexPattern; @@ -36,6 +37,7 @@ export const getTopNavLinks = ({ savedSearch: SavedSearch; services: DiscoverServices; state: GetStateReturn; + onOpenInspector: () => void; }) => { const newSearch = { id: 'new', @@ -123,6 +125,7 @@ export const getTopNavLinks = ({ }), testId: 'openInspectorButton', run: () => { + onOpenInspector(); services.inspector.open(inspectorAdapters, { title: savedSearch.title, }); diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index abc8086e72712b..b73f7391bf22a4 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -21,8 +21,8 @@ import { TimeRange, } from '../../../../data/public'; import { SavedSearch } from '../../saved_searches'; -import { AppState } from '../angular/discover_state'; -import { TopNavMenuData } from '../../../../navigation/public'; +import { AppState, GetStateReturn } from '../angular/discover_state'; +import { RequestAdapter } from '../../../../inspector/common'; export interface DiscoverProps { /** @@ -100,6 +100,22 @@ export interface DiscoverProps { * Client of uiSettings */ config: IUiSettingsClient; + /** + * returns field statistics based on the loaded data sample + */ + getFieldCounts: () => Promise>; + /** + * Use angular router for navigation + */ + navigateTo: () => void; + /** + * Functions to get/mutate state + */ + stateContainer: GetStateReturn; + /** + * Inspect, for analyzing requests and responses + */ + inspectorAdapters: { requests: RequestAdapter }; /** * Data plugin */ @@ -165,10 +181,6 @@ export interface DiscoverProps { * Currently selected time range */ timeRange?: { from: string; to: string }; - /** - * Menu data of top navigation (New, save ...) - */ - topNavMenu: TopNavMenuData[]; /** * Function to update the actual query */ diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 4f34bc6aa73b47..4dff5f1e0b5989 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -149,7 +149,8 @@ export const EditIndexPattern = withRouter( chrome.docTitle.change(indexPattern.title); const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); - + const kibana = useKibana(); + const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping; return (
@@ -182,11 +183,7 @@ export const EditIndexPattern = withRouter( defaultMessage="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch" values={{ indexPatternTitle: {indexPattern.title} }} />{' '} - + {mappingAPILink}

diff --git a/src/plugins/maps_legacy/public/components/wms_options.tsx b/src/plugins/maps_legacy/public/components/wms_options.tsx index b30f20d3552627..d4ed5abd896e4c 100644 --- a/src/plugins/maps_legacy/public/components/wms_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_options.tsx @@ -11,7 +11,6 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from '../index'; -import { Vis } from '../../../visualizations/public'; import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; import { WmsInternalOptions } from './wms_internal_options'; import { WMSOptions } from '../common/types'; @@ -19,14 +18,13 @@ import { WMSOptions } from '../common/types'; interface Props { stateParams: K; setValue: (title: 'wms', options: WMSOptions) => void; - vis: Vis; + tmsLayers: TmsLayer[]; } const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id }); -function WmsOptions({ stateParams, setValue, vis }: Props) { +function WmsOptions({ stateParams, setValue, tmsLayers }: Props) { const { wms } = stateParams; - const { tmsLayers } = vis.type.editorConfig.collections; const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]); const setWmsOption = (paramName: T, value: WMSOptions[T]) => diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index 94a113a2786c2d..a788b3c4d0b59f 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -51,7 +51,10 @@ export class NewsfeedPublicPlugin return { createNewsFeed$: (endpoint: NewsfeedApiEndpoint) => { const config = Object.assign({}, this.config, { - service: { pathTemplate: `/${endpoint}/v{VERSION}.json` }, + service: { + ...this.config.service, + pathTemplate: `/${endpoint}/v{VERSION}.json`, + }, }); return this.fetchNewsfeed(core, config); }, diff --git a/src/plugins/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx index 5b5b71c9e9f4ed..2bf13e46f70de7 100644 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -11,10 +11,12 @@ import { EuiIcon, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elast import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { truncatedColorSchemas } from '../../../charts/public'; import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; import { SelectOption, SwitchOption, NumberInputOption } from '../../../vis_default_editor/public'; import { WmsOptions } from '../../../maps_legacy/public'; import { RegionMapVisParams } from '../region_map_types'; +import { getTmsLayers, getVectorLayers } from '../kibana_services'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ text: name, @@ -26,14 +28,16 @@ const mapFieldForOption = ({ description, name }: FileLayerField) => ({ value: name, }); +const tmsLayers = getTmsLayers(); +const vectorLayers = getVectorLayers(); +const vectorLayerOptions = vectorLayers.map(mapLayerForOption); + export type RegionMapOptionsProps = { getServiceSettings: () => Promise; } & VisEditorOptionsProps; function RegionMapOptions(props: RegionMapOptionsProps) { - const { getServiceSettings, stateParams, vis, setValue } = props; - const { vectorLayers } = vis.type.editorConfig.collections; - const vectorLayerOptions = useMemo(() => vectorLayers.map(mapLayerForOption), [vectorLayers]); + const { getServiceSettings, stateParams, setValue } = props; const fieldOptions = useMemo( () => ((stateParams.selectedLayer && stateParams.selectedLayer.fields) || []).map( @@ -61,7 +65,7 @@ function RegionMapOptions(props: RegionMapOptionsProps) { setEmsHotLink(newLayer); } }, - [vectorLayers, setEmsHotLink, setValue] + [setEmsHotLink, setValue] ); const setField = useCallback( @@ -178,7 +182,7 @@ function RegionMapOptions(props: RegionMapOptionsProps) { label={i18n.translate('regionMap.visParams.colorSchemaLabel', { defaultMessage: 'Color schema', })} - options={vis.type.editorConfig.collections.colorSchemas} + options={truncatedColorSchemas} paramName="colorSchema" value={stateParams.colorSchema} setValue={setValue} @@ -197,7 +201,7 @@ function RegionMapOptions(props: RegionMapOptionsProps) { - + ); } diff --git a/src/plugins/region_map/public/kibana_services.ts b/src/plugins/region_map/public/kibana_services.ts index 60465e2e0c2512..77bc472e3b140c 100644 --- a/src/plugins/region_map/public/kibana_services.ts +++ b/src/plugins/region_map/public/kibana_services.ts @@ -12,6 +12,7 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { SharePluginStart } from '../../share/public'; +import { VectorLayer, TmsLayer } from '../../maps_legacy/public'; export const [getCoreService, setCoreService] = createGetterSetter('Core'); @@ -32,3 +33,7 @@ export const [getShareService, setShareService] = createGetterSetter( 'KibanaLegacy' ); + +export const [getTmsLayers, setTmsLayers] = createGetterSetter('TmsLayers'); + +export const [getVectorLayers, setVectorLayers] = createGetterSetter('VectorLayers'); diff --git a/src/plugins/region_map/public/region_map_type.ts b/src/plugins/region_map/public/region_map_type.ts index 0e8df51b17c793..35f4cffca18d43 100644 --- a/src/plugins/region_map/public/region_map_type.ts +++ b/src/plugins/region_map/public/region_map_type.ts @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { VisTypeDefinition } from '../../visualizations/public'; -import { truncatedColorSchemas } from '../../charts/public'; import { ORIGIN } from '../../maps_legacy/public'; import { getDeprecationMessage } from './get_deprecation_message'; @@ -18,6 +17,7 @@ import { createRegionMapOptions } from './components'; import { toExpressionAst } from './to_ast'; import { RegionMapVisParams } from './region_map_types'; import { mapToLayerWithId } from './util'; +import { setTmsLayers, setVectorLayers } from './kibana_services'; export function createRegionMapTypeDefinition({ uiSettings, @@ -50,11 +50,6 @@ provided base maps, or add your own. Darker colors represent higher values.', }, editorConfig: { optionsTemplate: createRegionMapOptions(getServiceSettings), - collections: { - colorSchemas: truncatedColorSchemas, - vectorLayers: [], - tmsLayers: [], - }, schemas: [ { group: 'metrics', @@ -95,7 +90,9 @@ provided base maps, or add your own. Darker colors represent higher values.', setup: async (vis) => { const serviceSettings = await getServiceSettings(); const tmsLayers = await serviceSettings.getTMSServices(); - vis.type.editorConfig.collections.tmsLayers = tmsLayers; + setTmsLayers(tmsLayers); + setVectorLayers([]); + if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) { vis.params.wms.selectedTmsLayer = tmsLayers[0]; } @@ -122,9 +119,10 @@ provided base maps, or add your own. Darker colors represent higher values.', } }); - vis.type.editorConfig.collections.vectorLayers = [...vectorLayers, ...newLayers]; + const allVectorLayers = [...vectorLayers, ...newLayers]; + setVectorLayers(allVectorLayers); - [selectedLayer] = vis.type.editorConfig.collections.vectorLayers; + [selectedLayer] = allVectorLayers; selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined; if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) { diff --git a/src/plugins/tile_map/public/components/collections.ts b/src/plugins/tile_map/public/components/collections.ts new file mode 100644 index 00000000000000..f75d83c4a055f9 --- /dev/null +++ b/src/plugins/tile_map/public/components/collections.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 { i18n } from '@kbn/i18n'; +import { MapTypes } from '../utils/map_types'; + +export const collections = { + mapTypes: [ + { + value: MapTypes.ScaledCircleMarkers, + text: i18n.translate('tileMap.mapTypes.scaledCircleMarkersText', { + defaultMessage: 'Scaled circle markers', + }), + }, + { + value: MapTypes.ShadedCircleMarkers, + text: i18n.translate('tileMap.mapTypes.shadedCircleMarkersText', { + defaultMessage: 'Shaded circle markers', + }), + }, + { + value: MapTypes.ShadedGeohashGrid, + text: i18n.translate('tileMap.mapTypes.shadedGeohashGridText', { + defaultMessage: 'Shaded geohash grid', + }), + }, + { + value: MapTypes.Heatmap, + text: i18n.translate('tileMap.mapTypes.heatmapText', { + defaultMessage: 'Heatmap', + }), + }, + ], + legendPositions: [ + { + value: 'bottomleft', + text: i18n.translate('tileMap.legendPositions.bottomLeftText', { + defaultMessage: 'Bottom left', + }), + }, + { + value: 'bottomright', + text: i18n.translate('tileMap.legendPositions.bottomRightText', { + defaultMessage: 'Bottom right', + }), + }, + { + value: 'topleft', + text: i18n.translate('tileMap.legendPositions.topLeftText', { + defaultMessage: 'Top left', + }), + }, + { + value: 'topright', + text: i18n.translate('tileMap.legendPositions.topRightText', { + defaultMessage: 'Top right', + }), + }, + ], +}; diff --git a/src/plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx index 9164a4b0d6300d..dbe28f0e2c2dde 100644 --- a/src/plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/plugins/tile_map/public/components/tile_map_options.tsx @@ -17,20 +17,25 @@ import { SwitchOption, RangeOption, } from '../../../vis_default_editor/public'; +import { truncatedColorSchemas } from '../../../charts/public'; import { WmsOptions } from '../../../maps_legacy/public'; import { TileMapVisParams } from '../types'; import { MapTypes } from '../utils/map_types'; +import { getTmsLayers } from '../services'; +import { collections } from './collections'; export type TileMapOptionsProps = VisEditorOptionsProps; +const tmsLayers = getTmsLayers(); + function TileMapOptions(props: TileMapOptionsProps) { const { stateParams, setValue, vis } = props; useEffect(() => { if (!stateParams.mapType) { - setValue('mapType', vis.type.editorConfig.collections.mapTypes[0]); + setValue('mapType', collections.mapTypes[0].value); } - }, [setValue, stateParams.mapType, vis.type.editorConfig.collections.mapTypes]); + }, [setValue, stateParams.mapType]); return ( <> @@ -39,7 +44,7 @@ function TileMapOptions(props: TileMapOptionsProps) { label={i18n.translate('tileMap.visParams.mapTypeLabel', { defaultMessage: 'Map type', })} - options={vis.type.editorConfig.collections.mapTypes} + options={collections.mapTypes} paramName="mapType" value={stateParams.mapType} setValue={setValue} @@ -62,14 +67,14 @@ function TileMapOptions(props: TileMapOptionsProps) { label={i18n.translate('tileMap.visParams.colorSchemaLabel', { defaultMessage: 'Color schema', })} - options={vis.type.editorConfig.collections.colorSchemas} + options={truncatedColorSchemas} paramName="colorSchema" value={stateParams.colorSchema} setValue={setValue} /> )} - + - + ); } diff --git a/src/plugins/tile_map/public/services.ts b/src/plugins/tile_map/public/services.ts index 3e6dbb69c9403a..af23daf24f7f58 100644 --- a/src/plugins/tile_map/public/services.ts +++ b/src/plugins/tile_map/public/services.ts @@ -11,6 +11,7 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { SharePluginStart } from '../../share/public'; +import { TmsLayer } from '../../maps_legacy/public'; export const [getCoreService, setCoreService] = createGetterSetter('Core'); @@ -27,3 +28,5 @@ export const [getShareService, setShareService] = createGetterSetter( 'KibanaLegacy' ); + +export const [getTmsLayers, setTmsLayers] = createGetterSetter('TmsLayers'); diff --git a/src/plugins/tile_map/public/tile_map_type.ts b/src/plugins/tile_map/public/tile_map_type.ts index dc2cd418c28e28..5e71351f1bd563 100644 --- a/src/plugins/tile_map/public/tile_map_type.ts +++ b/src/plugins/tile_map/public/tile_map_type.ts @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import { VisTypeDefinition } from 'src/plugins/visualizations/public'; -import { truncatedColorSchemas } from '../../charts/public'; // @ts-expect-error import { supportsCssFilters } from './css_filters'; @@ -17,7 +16,7 @@ import { getDeprecationMessage } from './get_deprecation_message'; import { TileMapVisualizationDependencies } from './plugin'; import { toExpressionAst } from './to_ast'; import { TileMapVisParams } from './types'; -import { MapTypes } from './utils/map_types'; +import { setTmsLayers } from './services'; export function createTileMapTypeDefinition( dependencies: TileMapVisualizationDependencies @@ -50,62 +49,6 @@ export function createTileMapTypeDefinition( }, toExpressionAst, editorConfig: { - collections: { - colorSchemas: truncatedColorSchemas, - legendPositions: [ - { - value: 'bottomleft', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.bottomLeftText', { - defaultMessage: 'Bottom left', - }), - }, - { - value: 'bottomright', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.bottomRightText', { - defaultMessage: 'Bottom right', - }), - }, - { - value: 'topleft', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.topLeftText', { - defaultMessage: 'Top left', - }), - }, - { - value: 'topright', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.topRightText', { - defaultMessage: 'Top right', - }), - }, - ], - mapTypes: [ - { - value: MapTypes.ScaledCircleMarkers, - text: i18n.translate('tileMap.vis.editorConfig.mapTypes.scaledCircleMarkersText', { - defaultMessage: 'Scaled circle markers', - }), - }, - { - value: MapTypes.ShadedCircleMarkers, - text: i18n.translate('tileMap.vis.editorConfig.mapTypes.shadedCircleMarkersText', { - defaultMessage: 'Shaded circle markers', - }), - }, - { - value: MapTypes.ShadedGeohashGrid, - text: i18n.translate('tileMap.vis.editorConfig.mapTypes.shadedGeohashGridText', { - defaultMessage: 'Shaded geohash grid', - }), - }, - { - value: MapTypes.Heatmap, - text: i18n.translate('tileMap.vis.editorConfig.mapTypes.heatmapText', { - defaultMessage: 'Heatmap', - }), - }, - ], - tmsLayers: [], - }, optionsTemplate: TileMapOptionsLazy, schemas: [ { @@ -141,7 +84,7 @@ export function createTileMapTypeDefinition( return vis; } - vis.type.editorConfig.collections.tmsLayers = tmsLayers; + setTmsLayers(tmsLayers); if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) { vis.params.wms.selectedTmsLayer = tmsLayers[0]; } diff --git a/src/plugins/vis_default_editor/public/components/options/basic_options.tsx b/src/plugins/vis_default_editor/public/components/options/basic_options.tsx index 5d19b6dab4b82d..5cec0743b94fd6 100644 --- a/src/plugins/vis_default_editor/public/components/options/basic_options.tsx +++ b/src/plugins/vis_default_editor/public/components/options/basic_options.tsx @@ -19,18 +19,23 @@ interface BasicOptionsParams { legendPosition: string; } +type LegendPositions = Array<{ + value: string; + text: string; +}>; + function BasicOptions({ stateParams, setValue, - vis, -}: VisEditorOptionsProps) { + legendPositions, +}: VisEditorOptionsProps & { legendPositions: LegendPositions }) { return ( <> ) { const setMetricValue: ( @@ -137,14 +157,14 @@ function MetricVisOptions({ isDisabled={stateParams.metric.colorsRange.length === 1} isFullWidth={true} legend={metricColorModeLabel} - options={vis.type.editorConfig.collections.metricColorMode} + options={metricColorMode} onChange={setColorMode} /> => }, }, editorConfig: { - collections: { - metricColorMode: [ - { - id: ColorMode.None, - label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { - defaultMessage: 'None', - }), - }, - { - id: ColorMode.Labels, - label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { - defaultMessage: 'Labels', - }), - }, - { - id: ColorMode.Background, - label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { - defaultMessage: 'Background', - }), - }, - ], - colorSchemas, - }, optionsTemplate: MetricVisOptions, schemas: [ { diff --git a/src/plugins/vis_type_tagcloud/public/components/collections.ts b/src/plugins/vis_type_tagcloud/public/components/collections.ts new file mode 100644 index 00000000000000..d5dd3c7f2d252d --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/collections.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not 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 { TagCloudVisParams } from '../types'; + +interface Scales { + text: string; + value: TagCloudVisParams['scale']; +} + +interface Orientation { + text: string; + value: TagCloudVisParams['orientation']; +} + +interface Collections { + scales: Scales[]; + orientations: Orientation[]; +} + +export const collections: Collections = { + scales: [ + { + text: i18n.translate('visTypeTagCloud.scales.linearText', { + defaultMessage: 'Linear', + }), + value: 'linear', + }, + { + text: i18n.translate('visTypeTagCloud.scales.logText', { + defaultMessage: 'Log', + }), + value: 'log', + }, + { + text: i18n.translate('visTypeTagCloud.scales.squareRootText', { + defaultMessage: 'Square root', + }), + value: 'square root', + }, + ], + orientations: [ + { + text: i18n.translate('visTypeTagCloud.orientations.singleText', { + defaultMessage: 'Single', + }), + value: 'single', + }, + { + text: i18n.translate('visTypeTagCloud.orientations.rightAngledText', { + defaultMessage: 'Right angled', + }), + value: 'right angled', + }, + { + text: i18n.translate('visTypeTagCloud.orientations.multipleText', { + defaultMessage: 'Multiple', + }), + value: 'multiple', + }, + ], +}; diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index 549cbc8bfec84f..d5e005a6386806 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -13,8 +13,9 @@ import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; import { ValidatedDualRange } from '../../../kibana_react/public'; import { TagCloudVisParams } from '../types'; +import { collections } from './collections'; -function TagCloudOptions({ stateParams, setValue, vis }: VisEditorOptionsProps) { +function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps) { const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { setValue('minFontSize', Number(minFontSize)); setValue('maxFontSize', Number(maxFontSize)); @@ -29,7 +30,7 @@ function TagCloudOptions({ stateParams, setValue, vis }: VisEditorOptionsProps(paramName: T, value: ColorSchemaParams[T]) => { @@ -91,7 +90,7 @@ function RangesPanel({ ) { - const { stateParams, vis, uiState, setValue, setValidity, setTouched } = props; + const { stateParams, uiState, setValue, setValidity, setTouched } = props; const [valueAxis] = stateParams.valueAxes; const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10; const [isColorRangesValid, setIsColorRangesValid] = useState(false); @@ -65,7 +68,7 @@ function HeatmapOptions(props: VisEditorOptionsProps) { - + ) { ) { label={i18n.translate('visTypeVislib.controls.heatmapOptions.colorScaleLabel', { defaultMessage: 'Color scale', })} - options={vis.type.editorConfig.collections.scales} + options={heatmapCollections.scales} paramName="type" value={valueAxis.scale.type} setValue={setValueAxisScale} diff --git a/src/plugins/vis_type_vislib/public/editor/components/pie.tsx b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx index 9acadd4252a95b..6c84bc744676a9 100644 --- a/src/plugins/vis_type_vislib/public/editor/components/pie.tsx +++ b/src/plugins/vis_type_vislib/public/editor/components/pie.tsx @@ -14,10 +14,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; import { BasicOptions, SwitchOption } from '../../../../vis_default_editor/public'; -import { TruncateLabelsOption } from '../../../../vis_type_xy/public'; +import { TruncateLabelsOption, getPositions } from '../../../../vis_type_xy/public'; import { PieVisParams } from '../../pie'; +const legendPositions = getPositions(); + function PieOptions(props: VisEditorOptionsProps) { const { stateParams, setValue } = props; const setLabels = ( @@ -45,7 +47,7 @@ function PieOptions(props: VisEditorOptionsProps) { value={stateParams.isDonut} setValue={setValue} /> - + diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index cd4c03e5a84d14..315c4388a5cd3d 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -14,7 +14,6 @@ import { AggGroupNames } from '../../data/public'; import { VisTypeDefinition, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; import { Alignment, GaugeType, VislibChartType } from './types'; -import { getGaugeCollections } from './editor'; import { toExpressionAst } from './to_ast'; import { GaugeOptions } from './editor/components'; @@ -102,7 +101,6 @@ export const gaugeVisTypeDefinition: VisTypeDefinition = { }, }, editorConfig: { - collections: getGaugeCollections(), optionsTemplate: GaugeOptions, schemas: [ { diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index a31ba48704d509..aaeae4f675f3fd 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -12,7 +12,7 @@ import { AggGroupNames } from '../../data/public'; import { ColorMode, ColorSchemas } from '../../charts/public'; import { VisTypeDefinition } from '../../visualizations/public'; -import { getGaugeCollections, GaugeOptions } from './editor'; +import { GaugeOptions } from './editor'; import { toExpressionAst } from './to_ast'; import { GaugeType } from './types'; import { GaugeVisParams } from './gauge'; @@ -66,7 +66,6 @@ export const goalVisTypeDefinition: VisTypeDefinition = { }, }, editorConfig: { - collections: getGaugeCollections(), optionsTemplate: GaugeOptions, schemas: [ { diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index ca6dda547571c3..f804a78cbe4538 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -15,7 +15,7 @@ import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; import { VIS_EVENT_TO_TRIGGER, VisTypeDefinition } from '../../visualizations/public'; import { ValueAxis, ScaleType, AxisType } from '../../vis_type_xy/public'; -import { HeatmapOptions, getHeatmapCollections } from './editor'; +import { HeatmapOptions } from './editor'; import { TimeMarker } from './vislib/visualizations/time_marker'; import { CommonVislibParams, VislibChartType } from './types'; import { toExpressionAst } from './to_ast'; @@ -75,7 +75,6 @@ export const heatmapVisTypeDefinition: VisTypeDefinition = { }, }, editorConfig: { - collections: getHeatmapCollections(), optionsTemplate: HeatmapOptions, schemas: [ { diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index e00fae7c32f066..d1d8d2a5279feb 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -11,7 +11,6 @@ import { Position } from '@elastic/charts'; import { AggGroupNames } from '../../data/public'; import { VisTypeDefinition, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; -import { getPositions } from '../../vis_type_xy/public'; import { CommonVislibParams } from './types'; import { PieOptions } from './editor'; @@ -53,9 +52,6 @@ export const pieVisTypeDefinition: VisTypeDefinition = { }, }, editorConfig: { - collections: { - legendPositions: getPositions(), - }, optionsTemplate: PieOptions, schemas: [ { diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap index e9cd2b737b879c..56f35ae0211732 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap @@ -31,6 +31,22 @@ exports[`ChartOptions component should init with the default set of props 1`] = `; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap index 594511010b7453..abcbf1a4fd7d93 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -150,80 +150,6 @@ exports[`ValueAxesPanel component should init with the default set of props 1`] "type": "value", } } - vis={ - Object { - "type": Object { - "editorConfig": Object { - "collections": Object { - "axisModes": Array [ - Object { - "text": "Normal", - "value": "normal", - }, - Object { - "text": "Percentage", - "value": "percentage", - }, - Object { - "text": "Wiggle", - "value": "wiggle", - }, - Object { - "text": "Silhouette", - "value": "silhouette", - }, - ], - "interpolationModes": Array [ - Object { - "text": "Straight", - "value": "linear", - }, - Object { - "text": "Smoothed", - "value": "cardinal", - }, - Object { - "text": "Stepped", - "value": "step-after", - }, - ], - "positions": Array [ - Object { - "text": "Top", - "value": "top", - }, - Object { - "text": "Left", - "value": "left", - }, - Object { - "text": "Right", - "value": "right", - }, - Object { - "text": "Bottom", - "value": "bottom", - }, - ], - "scaleTypes": Array [ - Object { - "text": "Linear", - "value": "linear", - }, - Object { - "text": "Log", - "value": "log", - }, - Object { - "text": "Square root", - "value": "square root", - }, - ], - }, - }, - }, - } - } /> diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx index 17a504a25b05f6..066f053d4e1865 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { CategoryAxisPanel, CategoryAxisPanelProps } from './category_axis_panel'; import { CategoryAxis } from '../../../../types'; import { LabelOptions } from './label_options'; -import { categoryAxis, vis } from './mocks'; +import { categoryAxis } from './mocks'; import { Position } from '@elastic/charts'; describe('CategoryAxisPanel component', () => { @@ -27,7 +27,6 @@ describe('CategoryAxisPanel component', () => { defaultProps = { axis, - vis, onPositionChanged, setCategoryAxis, }; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx index 6c261137d9eb61..5ba35717e46f38 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx @@ -13,25 +13,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { Position } from '@elastic/charts'; -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; import { SelectOption, SwitchOption } from '../../../../../../vis_default_editor/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CategoryAxis } from '../../../../types'; +import { getPositions } from '../../../collections'; + +const positions = getPositions(); export interface CategoryAxisPanelProps { axis: CategoryAxis; onPositionChanged: (position: Position) => void; setCategoryAxis: (value: CategoryAxis) => void; - vis: VisEditorOptionsProps['vis']; } -function CategoryAxisPanel({ - axis, - onPositionChanged, - vis, - setCategoryAxis, -}: CategoryAxisPanelProps) { +function CategoryAxisPanel({ axis, onPositionChanged, setCategoryAxis }: CategoryAxisPanelProps) { const setAxis = useCallback( (paramName: T, value: CategoryAxis[T]) => { const updatedAxis = { @@ -78,7 +74,7 @@ function CategoryAxisPanel({ label={i18n.translate('visTypeXy.controls.pointSeries.categoryAxis.positionLabel', { defaultMessage: 'Position', })} - options={vis.type.editorConfig.collections.positions} + options={positions} paramName="position" value={axis.position} setValue={setPosition} diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx index 1e274dce7c2a8e..caf14e57fef7e3 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx @@ -12,7 +12,7 @@ import { shallow } from 'enzyme'; import { ChartOptions, ChartOptionsParams } from './chart_options'; import { SeriesParam, ChartMode } from '../../../../types'; import { LineOptions } from './line_options'; -import { valueAxis, seriesParam, vis } from './mocks'; +import { valueAxis, seriesParam } from './mocks'; import { ChartType } from '../../../../../common'; describe('ChartOptions component', () => { @@ -29,7 +29,6 @@ describe('ChartOptions component', () => { defaultProps = { index: 0, chart, - vis, valueAxes: [valueAxis], setParamByIndex, changeValueAxis, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx index 76604383db8c52..6f0b4fc5c9d229 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -11,13 +11,15 @@ import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { Vis } from '../../../../../../visualizations/public'; import { SelectOption } from '../../../../../../vis_default_editor/public'; import { SeriesParam, ValueAxis } from '../../../../types'; import { LineOptions } from './line_options'; import { SetParamByIndex, ChangeValueAxis } from '.'; import { ChartType } from '../../../../../common'; +import { getConfigCollections } from '../../../collections'; + +const collections = getConfigCollections(); export type SetChart = (paramName: T, value: SeriesParam[T]) => void; @@ -27,14 +29,12 @@ export interface ChartOptionsParams { changeValueAxis: ChangeValueAxis; setParamByIndex: SetParamByIndex; valueAxes: ValueAxis[]; - vis: Vis; } function ChartOptions({ chart, index, valueAxes, - vis, changeValueAxis, setParamByIndex, }: ChartOptionsParams) { @@ -90,7 +90,7 @@ function ChartOptions({ label={i18n.translate('visTypeXy.controls.pointSeries.series.chartTypeLabel', { defaultMessage: 'Chart type', })} - options={vis.type.editorConfig.collections.chartTypes} + options={collections.chartTypes} paramName="type" value={chart.type} setValue={setChart} @@ -102,7 +102,7 @@ function ChartOptions({ label={i18n.translate('visTypeXy.controls.pointSeries.series.modeLabel', { defaultMessage: 'Mode', })} - options={vis.type.editorConfig.collections.chartModes} + options={collections.chartModes} paramName="mode" value={chart.mode} setValue={setChart} @@ -118,7 +118,7 @@ function ChartOptions({ label={i18n.translate('visTypeXy.controls.pointSeries.series.lineModeLabel', { defaultMessage: 'Line mode', })} - options={vis.type.editorConfig.collections.interpolationModes} + options={collections.interpolationModes} paramName="interpolate" value={chart.interpolate} setValue={setChart} @@ -126,7 +126,7 @@ function ChartOptions({ )} - {chart.type === ChartType.Line && } + {chart.type === ChartType.Line && } ); } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx index c295d909863dc1..d25845f02e7a73 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/index.tsx @@ -326,14 +326,12 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) { setMultipleValidity={props.setMultipleValidity} seriesParams={stateParams.seriesParams} valueAxes={stateParams.valueAxes} - vis={vis} /> ) : null; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx index c8a5e6f17b1ed9..5497c46c1dd344 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.test.tsx @@ -12,7 +12,7 @@ import { shallow } from 'enzyme'; import { NumberInputOption } from '../../../../../../vis_default_editor/public'; import { LineOptions, LineOptionsParams } from './line_options'; -import { seriesParam, vis } from './mocks'; +import { seriesParam } from './mocks'; const LINE_WIDTH = 'lineWidth'; const DRAW_LINES = 'drawLinesBetweenPoints'; @@ -26,7 +26,6 @@ describe('LineOptions component', () => { defaultProps = { chart: { ...seriesParam }, - vis, setChart, }; }); diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx index b101ed1553a24e..140f190c77181e 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx @@ -11,7 +11,6 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { Vis } from '../../../../../../visualizations/public'; import { NumberInputOption, SelectOption, @@ -20,14 +19,16 @@ import { import { SeriesParam } from '../../../../types'; import { SetChart } from './chart_options'; +import { getInterpolationModes } from '../../../collections'; + +const interpolationModes = getInterpolationModes(); export interface LineOptionsParams { chart: SeriesParam; - vis: Vis; setChart: SetChart; } -function LineOptions({ chart, vis, setChart }: LineOptionsParams) { +function LineOptions({ chart, setChart }: LineOptionsParams) { const setLineWidth = useCallback( (paramName: 'lineWidth', value: number | '') => { setChart(paramName, value === '' ? undefined : value); @@ -57,7 +58,7 @@ function LineOptions({ chart, vis, setChart }: LineOptionsParams) { label={i18n.translate('visTypeXy.controls.pointSeries.series.lineModeLabel', { defaultMessage: 'Line mode', })} - options={vis.type.editorConfig.collections.interpolationModes} + options={interpolationModes} paramName="interpolate" value={chart.interpolate} setValue={setChart} diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts index 33e2af174753e3..7451f6dea9039b 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts @@ -20,12 +20,6 @@ import { AxisType, CategoryAxis, } from '../../../../types'; -import { - getScaleTypes, - getAxisModes, - getPositions, - getInterpolationModes, -} from '../../../collections'; import { ChartType } from '../../../../../common'; const defaultValueAxisId = 'ValueAxis-1'; @@ -85,16 +79,9 @@ const seriesParam: SeriesParam = { valueAxis: defaultValueAxisId, }; -const positions = getPositions(); -const axisModes = getAxisModes(); -const scaleTypes = getScaleTypes(); -const interpolationModes = getInterpolationModes(); - const vis = ({ type: { - editorConfig: { - collections: { scaleTypes, axisModes, positions, interpolationModes }, - }, + editorConfig: {}, }, } as any) as Vis; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx index 13dab168e586cd..3e1a44993235b9 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.test.tsx @@ -14,7 +14,7 @@ import { Position } from '@elastic/charts'; import { ValueAxis, SeriesParam } from '../../../../types'; import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; -import { valueAxis, seriesParam, vis } from './mocks'; +import { valueAxis, seriesParam } from './mocks'; describe('ValueAxesPanel component', () => { let setParamByIndex: jest.Mock; @@ -53,7 +53,6 @@ describe('ValueAxesPanel component', () => { defaultProps = { seriesParams: [seriesParamCount, seriesParamAverage], valueAxes: [axisLeft, axisRight], - vis, setParamByIndex, onValueAxisPositionChanged, addValueAxis, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx index 5f874e0489370b..02bdb7b1852880 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axes_panel.tsx @@ -20,8 +20,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../../visualizations/public'; - import { SeriesParam, ValueAxis } from '../../../../types'; import { ValueAxisOptions } from './value_axis_options'; import { SetParamByIndex } from '.'; @@ -33,7 +31,6 @@ export interface ValueAxesPanelProps { setParamByIndex: SetParamByIndex; seriesParams: SeriesParam[]; valueAxes: ValueAxis[]; - vis: Vis; setMultipleValidity: (paramName: string, isValid: boolean) => void; } @@ -152,7 +149,6 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { onValueAxisPositionChanged={props.onValueAxisPositionChanged} setParamByIndex={props.setParamByIndex} setMultipleValidity={props.setMultipleValidity} - vis={props.vis} /> diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx index b843e7b5ab0640..f2d689126166f3 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.test.tsx @@ -16,7 +16,7 @@ import { TextInputOption } from '../../../../../../vis_default_editor/public'; import { ValueAxis, ScaleType } from '../../../../types'; import { LabelOptions } from './label_options'; import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; -import { valueAxis, vis } from './mocks'; +import { valueAxis } from './mocks'; const POSITION = 'position'; @@ -37,7 +37,6 @@ describe('ValueAxisOptions component', () => { axis, index: 0, valueAxis, - vis, setParamByIndex, onValueAxisPositionChanged, setMultipleValidity, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx index d9e0302cbe516e..1a38be83b9fc5e 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/value_axis_options.tsx @@ -10,7 +10,6 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; -import { Vis } from '../../../../../../visualizations/public'; import { SelectOption, SwitchOption, @@ -21,6 +20,9 @@ import { ValueAxis } from '../../../../types'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; import { SetParamByIndex } from '.'; +import { getConfigCollections } from '../../../collections'; + +const collections = getConfigCollections(); export type SetScale = ( paramName: T, @@ -33,7 +35,6 @@ export interface ValueAxisOptionsParams { onValueAxisPositionChanged: (index: number, value: ValueAxis['position']) => void; setParamByIndex: SetParamByIndex; valueAxis: ValueAxis; - vis: Vis; setMultipleValidity: (paramName: string, isValid: boolean) => void; } @@ -41,7 +42,6 @@ export function ValueAxisOptions({ axis, index, valueAxis, - vis, onValueAxisPositionChanged, setParamByIndex, setMultipleValidity, @@ -101,7 +101,7 @@ export function ValueAxisOptions({ label={i18n.translate('visTypeXy.controls.pointSeries.valueAxes.positionLabel', { defaultMessage: 'Position', })} - options={vis.type.editorConfig.collections.positions} + options={collections.positions} paramName="position" value={axis.position} setValue={onPositionChanged} @@ -112,7 +112,7 @@ export function ValueAxisOptions({ label={i18n.translate('visTypeXy.controls.pointSeries.valueAxes.modeLabel', { defaultMessage: 'Mode', })} - options={vis.type.editorConfig.collections.axisModes} + options={collections.axisModes} paramName="mode" value={axis.scale.mode} setValue={setValueAxisScale} @@ -123,7 +123,7 @@ export function ValueAxisOptions({ label={i18n.translate('visTypeXy.controls.pointSeries.valueAxes.scaleTypeLabel', { defaultMessage: 'Scale type', })} - options={vis.type.editorConfig.collections.scaleTypes} + options={collections.scaleTypes} paramName="type" value={axis.scale.type} setValue={setValueAxisScale} diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx index ecfbdf5b60528a..5398980e268d48 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -22,11 +22,14 @@ import { ChartType } from '../../../../../common'; import { VisParams } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; import { getPalettesService, getTrackUiMetric } from '../../../../services'; +import { getFittingFunctions } from '../../../collections'; + +const fittingFunctions = getFittingFunctions(); export function ElasticChartsOptions(props: ValidationVisOptionsProps) { const trackUiMetric = getTrackUiMetric(); const [palettesRegistry, setPalettesRegistry] = useState(null); - const { stateParams, setValue, vis, aggs } = props; + const { stateParams, setValue, aggs } = props; const hasLineChart = stateParams.seriesParams.some( ({ type, data: { id: paramId } }) => @@ -69,7 +72,7 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps label={i18n.translate('visTypeXy.editors.elasticChartsOptions.missingValuesLabel', { defaultMessage: 'Fill missing values', })} - options={vis.type.editorConfig.collections.fittingFunctions} + options={fittingFunctions} paramName="fittingFunction" value={stateParams.fittingFunction} setValue={(paramName, value) => { diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx index 27e940e62489ad..343976651d21e1 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx @@ -20,6 +20,9 @@ import { ThresholdPanel } from './threshold_panel'; import { ChartType } from '../../../../../common'; import { ValidationVisOptionsProps } from '../../common'; import { ElasticChartsOptions } from './elastic_charts_options'; +import { getPositions } from '../../../collections'; + +const legendPositions = getPositions(); export function PointSeriesOptions( props: ValidationVisOptionsProps< @@ -54,7 +57,7 @@ export function PointSeriesOptions( - + {vis.data.aggs!.aggs.some( (agg) => agg.schema === 'segment' && agg.type.name === BUCKET_TYPES.DATE_HISTOGRAM diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx index 943280b1373fb7..dadbe4dd1fc768 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/threshold_panel.tsx @@ -19,12 +19,14 @@ import { } from '../../../../../../vis_default_editor/public'; import { ValidationVisOptionsProps } from '../../common'; import { VisParams } from '../../../../types'; +import { getThresholdLineStyles } from '../../../collections'; + +const thresholdLineStyles = getThresholdLineStyles(); function ThresholdPanel({ stateParams, setValue, setMultipleValidity, - vis, }: ValidationVisOptionsProps) { const setThresholdLine = useCallback( ( @@ -94,7 +96,7 @@ function ThresholdPanel({ label={i18n.translate('visTypeXy.editors.pointSeries.thresholdLine.styleLabel', { defaultMessage: 'Line style', })} - options={vis.type.editorConfig.collections.thresholdLineStyles} + options={thresholdLineStyles} paramName="style" value={stateParams.thresholdLine.style} setValue={setThresholdLine} 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 b45c30b46c79eb..c425eb71117e85 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 @@ -1417,128 +1417,6 @@ export const sampleAreaVis = { }, }, editorConfig: { - collections: { - legendPositions: [ - { - text: 'Top', - value: 'top', - }, - { - text: 'Left', - value: 'left', - }, - { - text: 'Right', - value: 'right', - }, - { - text: 'Bottom', - value: 'bottom', - }, - ], - positions: [ - { - text: 'Top', - value: 'top', - }, - { - text: 'Left', - value: 'left', - }, - { - text: 'Right', - value: 'right', - }, - { - text: 'Bottom', - value: 'bottom', - }, - ], - chartTypes: [ - { - text: 'Line', - value: 'line', - }, - { - text: 'Area', - value: 'area', - }, - { - text: 'Bar', - value: 'histogram', - }, - ], - axisModes: [ - { - text: 'Normal', - value: 'normal', - }, - { - text: 'Percentage', - value: 'percentage', - }, - { - text: 'Wiggle', - value: 'wiggle', - }, - { - text: 'Silhouette', - value: 'silhouette', - }, - ], - scaleTypes: [ - { - text: 'Linear', - value: 'linear', - }, - { - text: 'Log', - value: 'log', - }, - { - text: 'Square root', - value: 'square root', - }, - ], - chartModes: [ - { - text: 'Normal', - value: 'normal', - }, - { - text: 'Stacked', - value: 'stacked', - }, - ], - interpolationModes: [ - { - text: 'Straight', - value: 'linear', - }, - { - text: 'Smoothed', - value: 'cardinal', - }, - { - text: 'Stepped', - value: 'step-after', - }, - ], - thresholdLineStyles: [ - { - value: 'full', - text: 'Full', - }, - { - value: 'dashed', - text: 'Dashed', - }, - { - value: 'dot-dashed', - text: 'Dot-dashed', - }, - ], - }, optionTabs: [ { name: 'advanced', diff --git a/src/plugins/vis_type_xy/public/vis_types/area.ts b/src/plugins/vis_type_xy/public/vis_types/area.ts index a118afb12d2496..a61c25bbc075a4 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.ts +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -26,7 +26,6 @@ import { } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; export const getAreaVisTypeDefinition = ( @@ -126,7 +125,6 @@ export const getAreaVisTypeDefinition = ( }, }, editorConfig: { - collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), schemas: [ { diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.ts b/src/plugins/vis_type_xy/public/vis_types/histogram.ts index 72d34f70b1a13f..2c2a83b48802d3 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -25,7 +25,6 @@ import { } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; @@ -129,7 +128,6 @@ export const getHistogramVisTypeDefinition = ( }, }, editorConfig: { - collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), schemas: [ { diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index 751803c07aa8de..75c4ddd75d0b3b 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -25,7 +25,6 @@ import { } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; @@ -128,7 +127,6 @@ export const getHorizontalBarVisTypeDefinition = ( }, }, editorConfig: { - collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), schemas: [ { diff --git a/src/plugins/vis_type_xy/public/vis_types/line.ts b/src/plugins/vis_type_xy/public/vis_types/line.ts index 75e4ebe09e3f76..87165a20592e59 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.ts +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -26,7 +26,6 @@ import { } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; export const getLineVisTypeDefinition = ( @@ -126,7 +125,6 @@ export const getLineVisTypeDefinition = ( }, }, editorConfig: { - collections: getConfigCollections(), optionTabs: getOptionTabs(showElasticChartsOptions), schemas: [ { diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 8dceee8e0010ad..6241f9ee4ae122 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -20,7 +20,6 @@ import { PersistedState } from './persisted_state'; import { VisParams } from '../common'; export { Vis, SerializedVis, VisParams }; - export interface SavedVisState { title: string; type: string; diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index c13c5d9accef33..c772554344cb26 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -40,6 +40,7 @@ export const VisualizeListing = () => { savedObjectsTagging, uiSettings, visualizeCapabilities, + dashboardCapabilities, kbnUrlStateStorage, }, } = useKibana(); @@ -172,11 +173,12 @@ export const VisualizeListing = () => { return ( <> - {dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && ( -
- -
- )} + {dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && + dashboardCapabilities.createNew && ( +
+ +
+ )} diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index d20553ee73e9c7..67c3d22d954265 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -83,7 +83,8 @@ export interface VisualizeServices extends CoreStart { navigation: NavigationStart; toastNotifications: ToastsStart; share?: SharePluginStart; - visualizeCapabilities: any; + visualizeCapabilities: Record>; + dashboardCapabilities: Record>; visualizations: VisualizationsStart; savedObjectsPublic: SavedObjectsStart; savedVisualizations: VisualizationsStart['savedVisualizationsLoader']; diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 3fd6fd15e36671..9ea42e8b565597 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -250,7 +250,7 @@ export const getTopNavConfig = ( share.toggleShareContextMenu({ anchorElement, allowEmbed: true, - allowShortUrl: visualizeCapabilities.createShortUrl, + allowShortUrl: Boolean(visualizeCapabilities.createShortUrl), shareableUrl: unhashUrl(window.location.href), objectId: savedVis?.id, objectType: 'visualization', diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index d93601ccd673ef..39074735e2aeb2 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -172,6 +172,7 @@ export class VisualizePlugin share: pluginsStart.share, toastNotifications: coreStart.notifications.toasts, visualizeCapabilities: coreStart.application.capabilities.visualize, + dashboardCapabilities: coreStart.application.capabilities.dashboard, visualizations: pluginsStart.visualizations, embeddable: pluginsStart.embeddable, stateTransferService: pluginsStart.embeddable.getStateTransfer(), diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index 881fa33f5fbc03..c2286f8ea3dce6 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -22,7 +22,8 @@ export default function ({ getService }: FtrProviderContext) { count, }); - describe('UI Counters API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/85086 + describe.skip('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); it('stores ui counter events in savedObjects', async () => { diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts deleted file mode 100644 index dab9dfce5e58af..00000000000000 --- a/x-pack/plugins/apm/common/projections.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export enum Projection { - services = 'services', - transactionGroups = 'transactionGroups', - traces = 'traces', - transactions = 'transactions', - metrics = 'metrics', - errorGroups = 'errorGroups', - serviceNodes = 'serviceNodes', - rumOverview = 'rumOverview', -} diff --git a/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts b/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts index 6aee1e2b9842de..9efb7184f39275 100644 --- a/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts +++ b/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts @@ -170,37 +170,52 @@ describe('date time formatters', () => { it('milliseconds', () => { const start = moment('2019-10-29 08:00:00.001'); const end = moment('2019-10-29 08:00:00.005'); - expect(getDateDifference(start, end, 'milliseconds')).toEqual(4); + expect( + getDateDifference({ start, end, unitOfTime: 'milliseconds' }) + ).toEqual(4); }); it('seconds', () => { const start = moment('2019-10-29 08:00:00'); const end = moment('2019-10-29 08:00:10'); - expect(getDateDifference(start, end, 'seconds')).toEqual(10); + expect(getDateDifference({ start, end, unitOfTime: 'seconds' })).toEqual( + 10 + ); }); it('minutes', () => { const start = moment('2019-10-29 08:00:00'); const end = moment('2019-10-29 08:15:00'); - expect(getDateDifference(start, end, 'minutes')).toEqual(15); + expect(getDateDifference({ start, end, unitOfTime: 'minutes' })).toEqual( + 15 + ); }); it('hours', () => { const start = moment('2019-10-29 08:00:00'); const end = moment('2019-10-29 10:00:00'); - expect(getDateDifference(start, end, 'hours')).toEqual(2); + expect(getDateDifference({ start, end, unitOfTime: 'hours' })).toEqual(2); }); it('days', () => { const start = moment('2019-10-29 08:00:00'); const end = moment('2019-10-30 10:00:00'); - expect(getDateDifference(start, end, 'days')).toEqual(1); + expect(getDateDifference({ start, end, unitOfTime: 'days' })).toEqual(1); }); it('months', () => { const start = moment('2019-10-29 08:00:00'); const end = moment('2019-12-29 08:00:00'); - expect(getDateDifference(start, end, 'months')).toEqual(2); + expect(getDateDifference({ start, end, unitOfTime: 'months' })).toEqual( + 2 + ); }); it('years', () => { const start = moment('2019-10-29 08:00:00'); const end = moment('2020-10-29 08:00:00'); - expect(getDateDifference(start, end, 'years')).toEqual(1); + expect(getDateDifference({ start, end, unitOfTime: 'years' })).toEqual(1); + }); + it('precise days', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2019-10-30 10:00:00'); + expect( + getDateDifference({ start, end, unitOfTime: 'days', precise: true }) + ).toEqual(1.0833333333333333); }); }); }); diff --git a/x-pack/plugins/apm/common/utils/formatters/datetime.ts b/x-pack/plugins/apm/common/utils/formatters/datetime.ts index 624a0b8a664bcd..88f70753f47c82 100644 --- a/x-pack/plugins/apm/common/utils/formatters/datetime.ts +++ b/x-pack/plugins/apm/common/utils/formatters/datetime.ts @@ -58,37 +58,43 @@ function getDateFormat(dateUnit: DateUnit) { } } -export const getDateDifference = ( - start: moment.Moment, - end: moment.Moment, - unitOfTime: DateUnit | TimeUnit -) => end.diff(start, unitOfTime); +export const getDateDifference = ({ + start, + end, + unitOfTime, + precise, +}: { + start: moment.Moment; + end: moment.Moment; + unitOfTime: DateUnit | TimeUnit; + precise?: boolean; +}) => end.diff(start, unitOfTime, precise); function getFormatsAccordingToDateDifference( start: moment.Moment, end: moment.Moment ) { - if (getDateDifference(start, end, 'years') >= 5) { + if (getDateDifference({ start, end, unitOfTime: 'years' }) >= 5) { return { dateFormat: getDateFormat('years') }; } - if (getDateDifference(start, end, 'months') >= 5) { + if (getDateDifference({ start, end, unitOfTime: 'months' }) >= 5) { return { dateFormat: getDateFormat('months') }; } const dateFormatWithDays = getDateFormat('days'); - if (getDateDifference(start, end, 'days') > 1) { + if (getDateDifference({ start, end, unitOfTime: 'days' }) > 1) { return { dateFormat: dateFormatWithDays }; } - if (getDateDifference(start, end, 'minutes') >= 1) { + if (getDateDifference({ start, end, unitOfTime: 'minutes' }) >= 1) { return { dateFormat: dateFormatWithDays, timeFormat: getTimeFormat('minutes'), }; } - if (getDateDifference(start, end, 'seconds') >= 10) { + if (getDateDifference({ start, end, unitOfTime: 'seconds' }) >= 10) { return { dateFormat: dateFormatWithDays, timeFormat: getTimeFormat('seconds'), diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index bb3903727f5092..834c2d5c40bcec 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -16,7 +16,7 @@ import { useTraceOverviewHref } from '../../shared/Links/apm/TraceOverviewLink'; import { MainTabs } from '../../shared/main_tabs'; import { ServiceMap } from '../ServiceMap'; import { ServiceInventory } from '../service_inventory'; -import { TraceOverview } from '../TraceOverview'; +import { TraceOverview } from '../trace_overview'; interface Tab { key: string; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 0fd85df37bb787..08d95aca24714b 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -23,7 +23,7 @@ import { AnomalyDetection } from '../../Settings/anomaly_detection'; import { ApmIndices } from '../../Settings/ApmIndices'; import { CustomizeUI } from '../../Settings/CustomizeUI'; import { TraceLink } from '../../TraceLink'; -import { TransactionDetails } from '../../TransactionDetails'; +import { TransactionDetails } from '../../transaction_details'; import { CreateAgentConfigurationRouteHandler, EditAgentConfigurationRouteHandler, diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx index 6423d295da4691..6bc345ea5bd87d 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { unit, px, truncate } from '../../../../style/variables'; +import { unit, px, truncate } from '../../../../../style/variables'; const BadgeText = styled.div` display: inline-block; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx index 59ec3b683b4d34..e1debde1117f96 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx @@ -21,7 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FilterBadgeList } from './FilterBadgeList'; -import { unit, px } from '../../../../style/variables'; +import { unit, px } from '../../../../../style/variables'; import { FilterTitleButton } from './FilterTitleButton'; const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx similarity index 89% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index 0cab58bc5f448f..a07997fb74921d 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -15,12 +15,10 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { Filter } from './Filter'; -import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { Projection } from '../../../../common/projections'; -import { LocalUIFilterName } from '../../../../common/ui_filter'; +import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; interface Props { - projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; showCount?: boolean; @@ -33,7 +31,6 @@ const ButtonWrapper = styled.div` `; function LocalUIFilters({ - projection, params, filterNames, children, @@ -42,7 +39,6 @@ function LocalUIFilters({ }: Props) { const { filters, setFilterValue, clearValues } = useLocalUIFilters({ filterNames, - projection, params, shouldFetch, }); diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts similarity index 76% rename from x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts index 1e0aa4fd961719..3f366300792aca 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts @@ -7,19 +7,21 @@ import { omit } from 'lodash'; import { useHistory } from 'react-router-dom'; -import { Projection } from '../../common/projections'; -import { pickKeys } from '../../common/utils/pick_keys'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; +import { LocalUIFiltersAPIResponse } from '../../../../../server/lib/ui_filters/local_ui_filters'; import { localUIFilters, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/ui_filters/local_ui_filters/config'; -import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; -import { removeUndefinedProps } from '../context/url_params_context/helpers'; -import { useFetcher } from './use_fetcher'; -import { useUrlParams } from '../context/url_params_context/use_url_params'; -import { LocalUIFilterName } from '../../common/ui_filter'; +} from '../../../../../server/lib/ui_filters/local_ui_filters/config'; +import { + fromQuery, + toQuery, +} from '../../../../components/shared/Links/url_helpers'; +import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; const getInitialData = ( filterNames: LocalUIFilterName[] @@ -31,12 +33,10 @@ const getInitialData = ( }; export function useLocalUIFilters({ - projection, filterNames, params, shouldFetch, }: { - projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; shouldFetch: boolean; @@ -72,7 +72,7 @@ export function useLocalUIFilters({ (callApmApi) => { if (shouldFetch && urlParams.start && urlParams.end) { return callApmApi({ - endpoint: `GET /api/apm/ui_filters/local_filters/${projection}` as const, + endpoint: `GET /api/apm/ui_filters/local_filters/rumOverview`, params: { query: { uiFilters: JSON.stringify(uiFilters), @@ -87,7 +87,6 @@ export function useLocalUIFilters({ } }, [ - projection, uiFilters, urlParams.start, urlParams.end, diff --git a/x-pack/plugins/apm/public/hooks/useCallApi.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts similarity index 68% rename from x-pack/plugins/apm/public/hooks/useCallApi.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts index a2bb77c6ad6fc4..5b448871804ebf 100644 --- a/x-pack/plugins/apm/public/hooks/useCallApi.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts @@ -6,9 +6,9 @@ */ import { useMemo } from 'react'; -import { callApi } from '../services/rest/callApi'; -import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; -import { FetchOptions } from '../../common/fetch_options'; +import { callApi } from '../../../../services/rest/callApi'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { FetchOptions } from '../../../../../common/fetch_options'; export function useCallApi() { const { http } = useApmPluginContext().core; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 7b0b1d204ac4d3..9bdad14eb8a188 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -5,13 +5,11 @@ * 2.0. */ -import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; +import { LocalUIFilters } from './LocalUIFilters'; import { RumDashboard } from './RumDashboard'; - -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { URLFilter } from './URLFilter'; export function RumOverview() { @@ -21,7 +19,6 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['location', 'device', 'os', 'browser'], - projection: Projection.rumOverview, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__fixtures__/props.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/__fixtures__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx similarity index 59% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 58fea5e985fae6..29bdf6467e5447 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -7,29 +7,26 @@ import { EuiFlexGroup, - EuiFlexItem, EuiPage, EuiPanel, EuiSpacer, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; +import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; -import { useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; +import { useFetcher } from '../../../hooks/use_fetcher'; import { SearchBar } from '../../shared/search_bar'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; -import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; interface ErrorGroupOverviewProps { serviceName: string; } -function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { +export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { const { urlParams, uiFilters } = useUrlParams(); const { start, end, sortField, sortDirection } = urlParams; const { errorDistributionData } = useErrorGroupDistributionFetcher({ @@ -68,18 +65,6 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { }); useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps = { - filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], - params: { - serviceName, - }, - projection: Projection.errorGroups, - }; - - return config; - }, [serviceName]); - if (!errorDistributionData || !errorGroupListData) { return null; } @@ -88,41 +73,34 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { <> - - - - - - - - + + + + - + - - -

Errors

-
- + + +

Errors

+
+ - -
-
+ +
); } - -export { ErrorGroupOverview }; diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 1c8a33d1968b18..23f699b63d207f 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -20,9 +20,9 @@ import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOv import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link'; import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link'; import { MainTabs } from '../../shared/main_tabs'; -import { ErrorGroupOverview } from '../ErrorGroupOverview'; +import { ErrorGroupOverview } from '../error_group_overview'; import { ServiceMap } from '../ServiceMap'; -import { ServiceNodeOverview } from '../ServiceNodeOverview'; +import { ServiceNodeOverview } from '../service_node_overview'; import { ServiceMetrics } from '../service_metrics'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 4ba96b63c91f45..1cb420a8ac1949 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -13,21 +13,19 @@ import { EuiPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { NoServicesMessage } from './no_services_message'; import { ServiceList } from './ServiceList'; import { MLCallout } from './ServiceList/MLCallout'; import { useAnomalyDetectionJobsFetcher } from './use_anomaly_detection_jobs_fetcher'; -import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; const initialData = { items: [], @@ -100,16 +98,6 @@ export function ServiceInventory() { useTrackPageview({ app: 'apm', path: 'services_overview' }); useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( - () => ({ - filterNames: ['host', 'agentName'], - projection: Projection.services, - }), - [] - ); - const { anomalyDetectionJobsData, anomalyDetectionJobsStatus, @@ -132,33 +120,24 @@ export function ServiceInventory() { <> - - - - - - - {displayMlCallout ? ( - - setUserHasDismissedCallout(true)} + + {displayMlCallout ? ( + + setUserHasDismissedCallout(true)} /> + + ) : null} + + + - - ) : null} - - - - } - /> - - - + } + /> + diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 647792bb130461..69b4149625824d 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -20,7 +20,6 @@ import { MockApmPluginContextWrapper, } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters'; import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; @@ -76,13 +75,6 @@ describe('ServiceInventory', () => { // @ts-expect-error global.sessionStorage = new SessionStorageMock(); - jest.spyOn(useLocalUIFilters, 'useLocalUIFilters').mockReturnValue({ - filters: [], - setFilterValue: () => null, - clearValues: () => null, - status: FETCH_STATUS.SUCCESS, - }); - jest.spyOn(hook, 'useAnomalyDetectionJobsFetcher').mockReturnValue({ anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, diff --git a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx index d1e6cc0d84ac4f..44a5adf31d0b6c 100644 --- a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx @@ -7,19 +7,17 @@ import { EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSpacer, - EuiFlexGroup, } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; import { MetricsChart } from '../../shared/charts/metrics_chart'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; -import { Projection } from '../../../../common/projections'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; interface ServiceMetricsProps { @@ -37,47 +35,28 @@ export function ServiceMetrics({ }); const { start, end } = urlParams; - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( - () => ({ - filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], - params: { - serviceName, - }, - projection: Projection.metrics, - showCount: false, - }), - [serviceName] - ); - return ( <> - - - - - - - - {data.charts.map((chart) => ( - - - - - - ))} - - - - + + + + {data.charts.map((chart) => ( + + + + + + ))} + + + diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 01874c956e8f91..00d184f692e3bc 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -4,30 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPage, - EuiPanel, - EuiToolTip, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; -import { Projection } from '../../../../common/projections'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { asDynamicBytes, asInteger, asPercent, } from '../../../../common/utils/formatters'; -import { useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../hooks/use_fetcher'; import { px, truncate, unit } from '../../../style/variables'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; import { SearchBar } from '../../shared/search_bar'; @@ -47,19 +38,6 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { const { uiFilters, urlParams } = useUrlParams(); const { start, end } = urlParams; - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( - () => ({ - filterNames: ['host', 'containerId', 'podName'], - params: { - serviceName, - }, - projection: Projection.serviceNodes, - }), - [serviceName] - ); - const { data: items = [] } = useFetcher( (callApmApi) => { if (!start || !end) { @@ -164,27 +142,22 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { <> - - - - - - - - - + + + + diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx rename to x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx similarity index 66% rename from x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index 624aee1e924722..d29dad7a7e3dea 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -6,16 +6,14 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; -import { TraceList } from './TraceList'; import { Correlations } from '../Correlations'; +import { TraceList } from './TraceList'; type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; const DEFAULT_RESPONSE: TracesAPIResponse = { @@ -48,32 +46,22 @@ export function TraceOverview() { useTrackPageview({ app: 'apm', path: 'traces_overview' }); useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 }); - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps = { - filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: Projection.traces, - }; - - return config; - }, []); - return ( <> - - - - - - - - - - + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/PercentOfParent.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/PercentOfParent.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/TransactionTabs.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/TransactionTabs.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx similarity index 59% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index b155672405b9f5..d5f5eed311de89 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -14,26 +14,23 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { isEmpty, flatten } from 'lodash'; -import { useHistory } from 'react-router-dom'; -import { RouteComponentProps } from 'react-router-dom'; +import { flatten, isEmpty } from 'lodash'; +import React from 'react'; +import { RouteComponentProps, useHistory } from 'react-router-dom'; +import { useTrackPageview } from '../../../../../observability/public'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; -import { useWaterfallFetcher } from './use_waterfall_fetcher'; import { ApmHeader } from '../../shared/ApmHeader'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { TransactionDistribution } from './Distribution'; -import { WaterfallWithSummmary } from './WaterfallWithSummmary'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; -import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; -import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; -import { Correlations } from '../Correlations'; +import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { SearchBar } from '../../shared/search_bar'; +import { Correlations } from '../Correlations'; +import { TransactionDistribution } from './Distribution'; +import { useWaterfallFetcher } from './use_waterfall_fetcher'; +import { WaterfallWithSummmary } from './WaterfallWithSummmary'; interface Sample { traceId: string; @@ -46,7 +43,6 @@ export function TransactionDetails({ location, match, }: TransactionDetailsProps) { - const { serviceName } = match.params; const { urlParams } = useUrlParams(); const history = useHistory(); const { @@ -59,24 +55,11 @@ export function TransactionDetails({ exceedsMax, status: waterfallStatus, } = useWaterfallFetcher(); - const { transactionName, transactionType } = urlParams; + const { transactionName } = urlParams; useTrackPageview({ app: 'apm', path: 'transaction_details' }); useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps = { - filterNames: ['transactionResult', 'serviceVersion'], - projection: Projection.transactions, - params: { - transactionName, - transactionType, - serviceName, - }, - }; - return config; - }, [transactionName, transactionType, serviceName]); - const selectedSample = flatten( distributionData.buckets.map((bucket) => bucket.samples) ).find( @@ -116,45 +99,45 @@ export function TransactionDetails({ - - - - - - - - - - - - - - { - if (!isEmpty(bucket.samples)) { - selectSampleFromBucketClick(bucket.samples[0]); - } - }} - /> - - - - - - - - + + + + + + + + + + + + + + + { + if (!isEmpty(bucket.samples)) { + selectSampleFromBucketClick(bucket.samples[0]); + } + }} + /> + + + + + + + diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 08904da3966781..1f8b431d072b79 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -10,7 +10,6 @@ import { EuiCode, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiPage, EuiPanel, EuiSpacer, @@ -19,25 +18,23 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Location } from 'history'; -import React, { useMemo } from 'react'; +import React from 'react'; import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { IUrlParams } from '../../../context/url_params_context/types'; -import { useTransactionListFetcher } from './use_transaction_list'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; import { SearchBar } from '../../shared/search_bar'; +import { TransactionTypeSelect } from '../../shared/transaction_type_select'; import { Correlations } from '../Correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { UserExperienceCallout } from './user_experience_callout'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useTransactionListFetcher } from './use_transaction_list'; function getRedirectLocation({ location, @@ -68,7 +65,7 @@ interface TransactionOverviewProps { export function TransactionOverview({ serviceName }: TransactionOverviewProps) { const location = useLocation(); const { urlParams } = useUrlParams(); - const { transactionType, transactionTypes } = useApmServiceContext(); + const { transactionType } = useApmServiceContext(); // redirect to first transaction type useRedirect(getRedirectLocation({ location, transactionType, urlParams })); @@ -80,27 +77,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { transactionListStatus, } = useTransactionListFetcher(); - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( - () => ({ - shouldFetch: !!transactionType, - filterNames: [ - 'transactionResult', - 'host', - 'containerId', - 'podName', - 'serviceVersion', - ], - params: { - serviceName, - transactionType, - }, - projection: Projection.transactionGroups, - }), - [serviceName, transactionType] - ); - // TODO: improve urlParams typings. // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed if (!serviceName) { @@ -112,74 +88,92 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { - - - - - + + + + + + + +

+ {i18n.translate('xpack.apm.transactionOverviewTitle', { + defaultMessage: 'Transactions', + })} +

+
+
+ + + +
- -
-
- - {transactionType === TRANSACTION_PAGE_LOAD && ( - <> - - - - )} - - - - -

Transactions

-
- - {!transactionListData.isAggregationAccurate && ( - -

- - xpack.apm.ui.transactionGroupBucketSize - - ), - }} - /> + + + + + - - {i18n.translate( - 'xpack.apm.transactionCardinalityWarning.docsLink', - { defaultMessage: 'Learn more in the docs' } - )} - -

-
- )} + {transactionType === TRANSACTION_PAGE_LOAD && ( + <> + - -
-
+ + )} + + + + +

Transactions

+
+ + {!transactionListData.isAggregationAccurate && ( + +

+ + xpack.apm.ui.transactionGroupBucketSize + + ), + }} + /> + + + {i18n.translate( + 'xpack.apm.transactionCardinalityWarning.docsLink', + { defaultMessage: 'Learn more in the docs' } + )} + +

+
+ )} + + +
diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index e0b1a4cbd05d5e..7d0ada3e31bffc 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -136,7 +136,9 @@ describe('TransactionOverview', () => { expect(getByText(container, 'firstType')).toBeInTheDocument(); expect(getByText(container, 'secondType')).toBeInTheDocument(); - fireEvent.click(getByText(container, 'firstType')); + fireEvent.change(getByText(container, 'firstType').parentElement!, { + target: { value: 'firstType' }, + }); expect(history.push).toHaveBeenCalled(); expect(history.location.search).toEqual( diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx deleted file mode 100644 index 19eefca5ee27e7..00000000000000 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiHorizontalRule, - EuiRadioGroup, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { fromQuery, toQuery } from '../../Links/url_helpers'; - -interface Props { - transactionTypes: string[]; -} - -function TransactionTypeFilter({ transactionTypes }: Props) { - const history = useHistory(); - const { - urlParams: { transactionType }, - } = useUrlParams(); - - const options = transactionTypes.map((type) => ({ - id: type, - label: type, - })); - - return ( - <> - -

- {i18n.translate('xpack.apm.localFilters.titles.transactionType', { - defaultMessage: 'Transaction type', - })} -

-
- - - - { - const newLocation = { - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionType: selectedTransactionType, - }), - }; - history.push(newLocation); - }} - /> - - ); -} - -export { TransactionTypeFilter }; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index 94fc79dd2164e0..1ceccc5203fb2f 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { asDuration } from '../../../../common/utils/formatters'; -import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent'; +import { PercentOfParent } from '../../app/transaction_details/WaterfallWithSummmary/PercentOfParent'; interface Props { duration: number; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx index 28a581d09908e1..1411a264b065ef 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { AgentMarker } from './AgentMarker'; -import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; +import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; describe('AgentMarker', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index f669063f07545c..ad8b85ba70c9b7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -12,7 +12,7 @@ import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { Legend } from '../../Legend'; -import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; +import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; const NameContainer = styled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx index 29e553235e57b8..36634f97a3a450 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx @@ -14,7 +14,7 @@ import { expectTextsInDocument, renderWithTheme, } from '../../../../../utils/testHelpers'; -import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; import { ErrorMarker } from './ErrorMarker'; function Wrapper({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index c38cc079559964..393281b2bf848c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -16,7 +16,7 @@ import { } from '../../../../../../common/elasticsearch_fieldnames'; import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { px, unit, units } from '../../../../../style/variables'; -import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; import { Legend, Shape } from '../../Legend'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx index 16ded0b2402c44..f156d82f05a515 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx @@ -8,8 +8,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Marker } from './'; -import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; +import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; describe('Marker', () => { it('renders agent marker', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx index 14688fe7e0c61a..b426a10a7562d2 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx @@ -10,8 +10,8 @@ import styled from 'styled-components'; import { px } from '../../../../../style/variables'; import { AgentMarker } from './AgentMarker'; import { ErrorMarker } from './ErrorMarker'; -import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; +import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; interface Props { mark: ErrorMark | AgentMark; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx index 218bdde37abd08..428da80fb808a4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { VerticalGridLines, XYPlot } from 'react-vis'; import { useTheme } from '../../../../hooks/use_theme'; -import { Mark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks'; +import { Mark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks'; import { PlotValues } from './plotUtils'; interface VerticalLinesProps { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx index 84bdd7998cfad9..650faa195271c5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx @@ -11,8 +11,8 @@ import { makeWidthFlexible } from 'react-vis'; import { getPlotValues } from './plotUtils'; import { TimelineAxis } from './TimelineAxis'; import { VerticalLines } from './VerticalLines'; -import { ErrorMark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; -import { AgentMark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; +import { ErrorMark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { AgentMark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; export type Mark = AgentMark | ErrorMark; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 296ec3c2d32e99..34ba1d86264c17 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -16,7 +16,7 @@ import { useBreakPoints } from '../../hooks/use_break_points'; const SearchBarFlexGroup = styled(EuiFlexGroup)` margin: ${({ theme }) => - `${theme.eui.euiSizeM} ${theme.eui.euiSizeM} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeM}`}; + `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`}; `; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx index 52d971a5511440..4ace78f74ee79e 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -17,6 +17,7 @@ import { } from '../../../utils/testHelpers'; import { TimeComparison } from './'; import * as urlHelpers from '../../shared/Links/url_helpers'; +import moment from 'moment'; function getWrapper(params?: IUrlParams) { return ({ children }: { children?: ReactNode }) => { @@ -31,6 +32,10 @@ function getWrapper(params?: IUrlParams) { } describe('TimeComparison', () => { + beforeAll(() => { + moment.tz.setDefault('Europe/Amsterdam'); + }); + afterAll(() => moment.tz.setDefault('')); const spy = jest.spyOn(urlHelpers, 'replace'); beforeEach(() => { jest.resetAllMocks(); @@ -40,6 +45,7 @@ describe('TimeComparison', () => { const Wrapper = getWrapper({ start: '2021-01-28T14:45:00.000Z', end: '2021-01-28T15:00:00.000Z', + rangeTo: 'now', }); render(, { wrapper: Wrapper, @@ -57,6 +63,7 @@ describe('TimeComparison', () => { end: '2021-01-28T15:00:00.000Z', comparisonEnabled: true, comparisonType: 'yesterday', + rangeTo: 'now', }); const component = render(, { wrapper: Wrapper, @@ -67,13 +74,64 @@ describe('TimeComparison', () => { .selectedIndex ).toEqual(0); }); + + it('enables yesterday option when date difference is equal to 24 hours', () => { + const Wrapper = getWrapper({ + start: '2021-01-28T10:00:00.000Z', + end: '2021-01-29T10:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'yesterday', + rangeTo: 'now', + }); + const component = render(, { + wrapper: Wrapper, + }); + expectTextsInDocument(component, ['Yesterday', 'A week ago']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); + + it('selects previous period when rangeTo is different than now', () => { + const Wrapper = getWrapper({ + start: '2021-01-28T10:00:00.000Z', + end: '2021-01-29T10:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'previousPeriod', + rangeTo: 'now-15m', + }); + const component = render(, { + wrapper: Wrapper, + }); + expectTextsInDocument(component, ['28/01 11:00 - 29/01 11:00']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); }); describe('Time range is between 24 hours - 1 week', () => { + it("doesn't show yesterday option when date difference is greater than 24 hours", () => { + const Wrapper = getWrapper({ + start: '2021-01-28T10:00:00.000Z', + end: '2021-01-29T11:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'week', + rangeTo: 'now', + }); + const component = render(, { + wrapper: Wrapper, + }); + expectTextsNotInDocument(component, ['Yesterday']); + expectTextsInDocument(component, ['A week ago']); + }); it('sets default values', () => { const Wrapper = getWrapper({ start: '2021-01-26T15:00:00.000Z', end: '2021-01-28T15:00:00.000Z', + rangeTo: 'now', }); render(, { wrapper: Wrapper, @@ -91,6 +149,7 @@ describe('TimeComparison', () => { end: '2021-01-28T15:00:00.000Z', comparisonEnabled: true, comparisonType: 'week', + rangeTo: 'now', }); const component = render(, { wrapper: Wrapper, @@ -102,6 +161,24 @@ describe('TimeComparison', () => { .selectedIndex ).toEqual(0); }); + + it('selects previous period when rangeTo is different than now', () => { + const Wrapper = getWrapper({ + start: '2021-01-26T15:00:00.000Z', + end: '2021-01-28T15:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'previousPeriod', + rangeTo: '2021-01-28T15:00:00.000Z', + }); + const component = render(, { + wrapper: Wrapper, + }); + expectTextsInDocument(component, ['26/01 16:00 - 28/01 16:00']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); }); describe('Time range is greater than 7 days', () => { @@ -111,12 +188,13 @@ describe('TimeComparison', () => { end: '2021-01-28T15:00:00.000Z', comparisonEnabled: true, comparisonType: 'previousPeriod', + rangeTo: 'now', }); const component = render(, { wrapper: Wrapper, }); expect(spy).not.toHaveBeenCalled(); - expectTextsInDocument(component, ['20/01 - 28/01']); + expectTextsInDocument(component, ['20/01 16:00 - 28/01 16:00']); expect( (component.getByTestId('comparisonSelect') as HTMLSelectElement) .selectedIndex @@ -129,12 +207,13 @@ describe('TimeComparison', () => { end: '2021-01-28T15:00:00.000Z', comparisonEnabled: true, comparisonType: 'previousPeriod', + rangeTo: 'now', }); const component = render(, { wrapper: Wrapper, }); expect(spy).not.toHaveBeenCalled(); - expectTextsInDocument(component, ['20/12/20 - 28/01/21']); + expectTextsInDocument(component, ['20/12/20 16:00 - 28/01/21 16:00']); expect( (component.getByTestId('comparisonSelect') as HTMLSelectElement) .selectedIndex diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index bb50ca1a45e8ca..02064ea786fb09 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -33,14 +33,21 @@ function formatPreviousPeriodDates({ momentEnd: moment.Moment; }) { const isDifferentYears = momentStart.get('year') !== momentEnd.get('year'); - const dateFormat = isDifferentYears ? 'DD/MM/YY' : 'DD/MM'; + const dateFormat = isDifferentYears ? 'DD/MM/YY HH:mm' : 'DD/MM HH:mm'; return `${momentStart.format(dateFormat)} - ${momentEnd.format(dateFormat)}`; } -function getSelectOptions({ start, end }: { start?: string; end?: string }) { +function getSelectOptions({ + start, + end, + rangeTo, +}: { + start?: string; + end?: string; + rangeTo?: string; +}) { const momentStart = moment(start); const momentEnd = moment(end); - const dateDiff = getDateDifference(momentStart, momentEnd, 'days'); const yesterdayOption = { value: 'yesterday', @@ -56,22 +63,32 @@ function getSelectOptions({ start, end }: { start?: string; end?: string }) { }), }; + const dateDiff = getDateDifference({ + start: momentStart, + end: momentEnd, + unitOfTime: 'days', + precise: true, + }); + const isRangeToNow = rangeTo === 'now'; + + if (isRangeToNow) { + // Less than or equals to one day + if (dateDiff <= 1) { + return [yesterdayOption, aWeekAgoOption]; + } + + // Less than or equals to one week + if (dateDiff <= 7) { + return [aWeekAgoOption]; + } + } + const prevPeriodOption = { value: 'previousPeriod', text: formatPreviousPeriodDates({ momentStart, momentEnd }), }; - // Less than one day - if (dateDiff < 1) { - return [yesterdayOption, aWeekAgoOption]; - } - - // Less than one week - if (dateDiff <= 7) { - return [aWeekAgoOption]; - } - - // above one week + // above one week or when rangeTo is not "now" return [prevPeriodOption]; } @@ -79,10 +96,10 @@ export function TimeComparison() { const history = useHistory(); const { isMedium, isLarge } = useBreakPoints(); const { - urlParams: { start, end, comparisonEnabled, comparisonType }, + urlParams: { start, end, comparisonEnabled, comparisonType, rangeTo }, } = useUrlParams(); - const selectOptions = getSelectOptions({ start, end }); + const selectOptions = getSelectOptions({ start, end, rangeTo }); // Sets default values if (comparisonEnabled === undefined || comparisonType === undefined) { @@ -113,7 +130,7 @@ export function TimeComparison() { 0} + checked={comparisonEnabled} onChange={() => { urlHelpers.push(history, { query: { diff --git a/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx b/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx new file mode 100644 index 00000000000000..772b42ed13577f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FormEvent, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; +import { useApmServiceContext } from '../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../context/url_params_context/use_url_params'; +import * as urlHelpers from './Links/url_helpers'; + +// The default transaction type (for non-RUM services) is "request". Set the +// min-width on here to the width when "request" is loaded so it doesn't start +// out collapsed and change its width when the list of transaction types is loaded. +const EuiSelectWithWidth = styled(EuiSelect)` + min-width: 157px; +`; + +export function TransactionTypeSelect() { + const { transactionTypes } = useApmServiceContext(); + const history = useHistory(); + const { + urlParams: { transactionType }, + } = useUrlParams(); + + const handleChange = useCallback( + (event: FormEvent) => { + const selectedTransactionType = event.currentTarget.value; + urlHelpers.push(history, { + query: { transactionType: selectedTransactionType }, + }); + }, + [history] + ); + + const options = transactionTypes.map((t) => ({ text: t, value: t })); + + return ( + <> + + + ); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 610442d4ff614e..5d580fc0e253a5 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -66,15 +66,8 @@ import { transactionThroughputChatsRoute, } from './transactions'; import { - errorGroupsLocalFiltersRoute, - metricsLocalFiltersRoute, - servicesLocalFiltersRoute, - tracesLocalFiltersRoute, - transactionGroupsLocalFiltersRoute, - transactionsLocalFiltersRoute, - serviceNodesLocalFiltersRoute, - uiFiltersEnvironmentsRoute, rumOverviewLocalFiltersRoute, + uiFiltersEnvironmentsRoute, } from './ui_filters'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; import { @@ -176,13 +169,6 @@ const createApmApi = () => { .add(transactionThroughputChatsRoute) // UI filters - .add(errorGroupsLocalFiltersRoute) - .add(metricsLocalFiltersRoute) - .add(servicesLocalFiltersRoute) - .add(tracesLocalFiltersRoute) - .add(transactionGroupsLocalFiltersRoute) - .add(transactionsLocalFiltersRoute) - .add(serviceNodesLocalFiltersRoute) .add(uiFiltersEnvironmentsRoute) // Service map diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 9cedbf16e161b4..b14a47e302caa9 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -7,29 +7,23 @@ import * as t from 'io-ts'; import { omit } from 'lodash'; +import { jsonRt } from '../../common/runtime_types/json_rt'; +import { LocalUIFilterName } from '../../common/ui_filter'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter'; import { - setupRequest, Setup, + setupRequest, SetupTimeRange, } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { Projection } from '../projections/typings'; -import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config'; -import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { getServicesProjection } from '../projections/services'; -import { getTransactionGroupsProjection } from '../projections/transaction_groups'; -import { getMetricsProjection } from '../projections/metrics'; -import { getErrorGroupsProjection } from '../projections/errors'; -import { getTransactionsProjection } from '../projections/transactions'; -import { createRoute } from './create_route'; -import { uiFiltersRt, rangeRt } from './default_api_types'; -import { jsonRt } from '../../common/runtime_types/json_rt'; -import { getServiceNodesProjection } from '../projections/service_nodes'; +import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config'; import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; -import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { Projection } from '../projections/typings'; +import { createRoute } from './create_route'; +import { rangeRt, uiFiltersRt } from './default_api_types'; import { APMRequestHandlerContext } from './typings'; -import { LocalUIFilterName } from '../../common/ui_filter'; export const uiFiltersEnvironmentsRoute = createRoute({ endpoint: 'GET /api/apm/ui_filters/environments', @@ -122,136 +116,6 @@ function createLocalFiltersRoute< }); } -export const servicesLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: `GET /api/apm/ui_filters/local_filters/services`, - getProjection: async ({ context, setup }) => { - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getServicesProjection({ setup, searchAggregatedTransactions }); - }, - queryRt: t.type({}), -}); - -export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/transactionGroups', - getProjection: async ({ context, setup, query }) => { - const { transactionType, serviceName, transactionName } = query; - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getTransactionGroupsProjection({ - setup, - options: { - type: 'top_transactions', - transactionType, - serviceName, - transactionName, - searchAggregatedTransactions, - }, - }); - }, - queryRt: t.intersection([ - t.type({ - serviceName: t.string, - transactionType: t.string, - }), - t.partial({ - transactionName: t.string, - }), - ]), -}); - -export const tracesLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/traces', - getProjection: async ({ setup, context }) => { - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getTransactionGroupsProjection({ - setup, - options: { type: 'top_traces', searchAggregatedTransactions }, - }); - }, - queryRt: t.type({}), -}); - -export const transactionsLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/transactions', - getProjection: async ({ context, setup, query }) => { - const { transactionType, serviceName, transactionName } = query; - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getTransactionsProjection({ - setup, - transactionType, - serviceName, - transactionName, - searchAggregatedTransactions, - }); - }, - queryRt: t.type({ - transactionType: t.string, - transactionName: t.string, - serviceName: t.string, - }), -}); - -export const metricsLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/metrics', - getProjection: ({ setup, query }) => { - const { serviceName, serviceNodeName } = query; - return getMetricsProjection({ - setup, - serviceName, - serviceNodeName, - }); - }, - queryRt: t.intersection([ - t.type({ - serviceName: t.string, - }), - t.partial({ - serviceNodeName: t.string, - }), - ]), -}); - -export const errorGroupsLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/errorGroups', - getProjection: ({ setup, query }) => { - const { serviceName } = query; - return getErrorGroupsProjection({ - setup, - serviceName, - }); - }, - queryRt: t.type({ - serviceName: t.string, - }), -}); - -export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/serviceNodes', - getProjection: ({ setup, query }) => { - const { serviceName } = query; - return getServiceNodesProjection({ - setup, - serviceName, - }); - }, - queryRt: t.type({ - serviceName: t.string, - }), -}); - export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ endpoint: 'GET /api/apm/ui_filters/local_filters/rumOverview', getProjection: async ({ setup }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts index 909d10aae68235..07e53d0d29282f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts @@ -7,3 +7,4 @@ export { RELEVANCE_TUNING_TITLE } from './constants'; export { RelevanceTuning } from './relevance_tuning'; +export { RelevanceTuningLogic } from './relevance_tuning_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts new file mode 100644 index 00000000000000..586a845ce382af --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -0,0 +1,297 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../__mocks__'; + +import { BoostType } from './types'; + +import { RelevanceTuningLogic } from './relevance_tuning_logic'; + +describe('RelevanceTuningLogic', () => { + const { mount } = new LogicMounter(RelevanceTuningLogic); + + const searchSettings = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + search_fields: {}, + }; + const schema = {}; + const schemaConflicts = {}; + const relevanceTuningProps = { + searchSettings, + schema, + schemaConflicts, + }; + const searchResults = [{}, {}]; + + const DEFAULT_VALUES = { + dataLoading: true, + schema: {}, + schemaConflicts: {}, + searchSettings: {}, + unsavedChanges: false, + filterInputValue: '', + query: '', + resultsLoading: false, + searchResults: null, + showSchemaConflictCallout: true, + engineHasSchemaFields: false, + schemaFields: [], + schemaFieldsWithConflicts: [], + filteredSchemaFields: [], + filteredSchemaFieldsWithConflicts: [], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(RelevanceTuningLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onInitializeRelevanceTuning', () => { + it('should set searchSettings, schema, & schemaConflicts from the API response, and set dataLoading to false', () => { + mount({ + dataLoading: true, + }); + RelevanceTuningLogic.actions.onInitializeRelevanceTuning(relevanceTuningProps); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + searchSettings, + schema, + dataLoading: false, + schemaConflicts, + }); + }); + }); + + describe('setSearchSettings', () => { + it('should set setSearchSettings and set unsavedChanges to true', () => { + mount({ + unsavedChanges: false, + }); + RelevanceTuningLogic.actions.setSearchSettings(searchSettings); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + searchSettings, + unsavedChanges: true, + }); + }); + }); + + describe('setFilterValue', () => { + it('should set filterInputValue', () => { + mount(); + RelevanceTuningLogic.actions.setFilterValue('foo'); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + filterInputValue: 'foo', + }); + }); + }); + + describe('setSearchQuery', () => { + it('should set query', () => { + mount(); + RelevanceTuningLogic.actions.setSearchQuery('foo'); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + query: 'foo', + }); + }); + }); + + describe('setSearchResults', () => { + it('should set searchResults and set resultLoading to false', () => { + mount({ + resultsLoading: true, + }); + RelevanceTuningLogic.actions.setSearchResults(searchResults); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + searchResults, + resultsLoading: false, + }); + }); + }); + + describe('setResultsLoading', () => { + it('should set resultsLoading', () => { + mount({ + resultsLoading: false, + }); + RelevanceTuningLogic.actions.setResultsLoading(true); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + resultsLoading: true, + }); + }); + }); + + describe('clearSearchResults', () => { + it('should set searchResults', () => { + mount({ + searchResults: [{}], + }); + RelevanceTuningLogic.actions.clearSearchResults(); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + searchResults: null, + }); + }); + }); + + describe('resetSearchSettingsState', () => { + it('should set dataLoading', () => { + mount({ + dataLoading: false, + }); + RelevanceTuningLogic.actions.resetSearchSettingsState(); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + }); + + describe('dismissSchemaConflictCallout', () => { + it('should set showSchemaConflictCallout to false', () => { + mount({ + showSchemaConflictCallout: true, + }); + RelevanceTuningLogic.actions.dismissSchemaConflictCallout(); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + showSchemaConflictCallout: false, + }); + }); + }); + }); + + describe('selectors', () => { + describe('engineHasSchemaFields', () => { + it('should return false if there is only a single field in a schema', () => { + // This is because if a schema only has a single field, it is "id", which we do not + // consider a tunable field. + mount({ + schema: { + id: 'foo', + }, + }); + expect(RelevanceTuningLogic.values.engineHasSchemaFields).toEqual(false); + }); + + it('should return true otherwise', () => { + mount({ + schema: { + id: 'foo', + bar: 'bar', + }, + }); + expect(RelevanceTuningLogic.values.engineHasSchemaFields).toEqual(true); + }); + }); + + describe('schemaFields', () => { + it('should return the list of field names from the schema', () => { + mount({ + schema: { + id: 'foo', + bar: 'bar', + }, + }); + expect(RelevanceTuningLogic.values.schemaFields).toEqual(['id', 'bar']); + }); + }); + + describe('schemaFieldsWithConflicts', () => { + it('should return the list of field names that have schema conflicts', () => { + mount({ + schemaConflicts: { + foo: { + text: ['source_engine_1'], + number: ['source_engine_2'], + }, + }, + }); + expect(RelevanceTuningLogic.values.schemaFieldsWithConflicts).toEqual(['foo']); + }); + }); + + describe('filteredSchemaFields', () => { + it('should return a list of schema field names that contain the text from filterInputValue ', () => { + mount({ + filterInputValue: 'ba', + schema: { + id: 'string', + foo: 'string', + bar: 'string', + baz: 'string', + }, + }); + expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual(['bar', 'baz']); + }); + + it('should return all schema fields if there is no filter applied', () => { + mount({ + filterTerm: '', + schema: { + id: 'string', + foo: 'string', + bar: 'string', + baz: 'string', + }, + }); + expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual([ + 'id', + 'foo', + 'bar', + 'baz', + ]); + }); + }); + + describe('filteredSchemaFieldsWithConflicts', () => { + it('should return a list of schema field names that contain the text from filterInputValue, and if that field has a schema conflict', () => { + mount({ + filterInputValue: 'ba', + schema: { + id: 'string', + foo: 'string', + bar: 'string', + baz: 'string', + }, + schemaConflicts: { + bar: { + text: ['source_engine_1'], + number: ['source_engine_2'], + }, + }, + }); + expect(RelevanceTuningLogic.values.filteredSchemaFieldsWithConflicts).toEqual(['bar']); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts new file mode 100644 index 00000000000000..d4ec5e37f6ce5e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { Schema, SchemaConflicts } from '../../../shared/types'; + +import { SearchSettings } from './types'; + +interface RelevanceTuningProps { + searchSettings: SearchSettings; + schema: Schema; + schemaConflicts: SchemaConflicts; +} + +interface RelevanceTuningActions { + onInitializeRelevanceTuning(props: RelevanceTuningProps): RelevanceTuningProps; + setSearchSettings(searchSettings: SearchSettings): { searchSettings: SearchSettings }; + setFilterValue(value: string): string; + setSearchQuery(value: string): string; + setSearchResults(searchResults: object[]): object[]; + setResultsLoading(resultsLoading: boolean): boolean; + clearSearchResults(): void; + resetSearchSettingsState(): void; + dismissSchemaConflictCallout(): void; +} + +interface RelevanceTuningValues { + searchSettings: Partial; + schema: Schema; + schemaFields: string[]; + schemaFieldsWithConflicts: string[]; + filteredSchemaFields: string[]; + filteredSchemaFieldsWithConflicts: string[]; + schemaConflicts: SchemaConflicts; + showSchemaConflictCallout: boolean; + engineHasSchemaFields: boolean; + filterInputValue: string; + query: string; + unsavedChanges: boolean; + dataLoading: boolean; + searchResults: object[] | null; + resultsLoading: boolean; +} + +// If the user hasn't entered a filter, then we can skip filtering the array entirely +const filterIfTerm = (array: string[], filterTerm: string): string[] => { + return filterTerm === '' ? array : array.filter((item) => item.includes(filterTerm)); +}; + +export const RelevanceTuningLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'relevance_tuning_logic'], + actions: () => ({ + onInitializeRelevanceTuning: (props) => props, + setSearchSettings: (searchSettings) => ({ searchSettings }), + setFilterValue: (value) => value, + setSearchQuery: (query) => query, + setSearchResults: (searchResults) => searchResults, + setResultsLoading: (resultsLoading) => resultsLoading, + clearSearchResults: true, + resetSearchSettingsState: true, + dismissSchemaConflictCallout: true, + }), + reducers: () => ({ + searchSettings: [ + {}, + { + onInitializeRelevanceTuning: (_, { searchSettings }) => searchSettings, + setSearchSettings: (_, { searchSettings }) => searchSettings, + }, + ], + schema: [ + {}, + { + onInitializeRelevanceTuning: (_, { schema }) => schema, + }, + ], + schemaConflicts: [ + {}, + { + onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts, + }, + ], + showSchemaConflictCallout: [ + true, + { + dismissSchemaConflictCallout: () => false, + }, + ], + filterInputValue: [ + '', + { + setFilterValue: (_, filterInputValue) => filterInputValue, + }, + ], + query: [ + '', + { + setSearchQuery: (_, query) => query, + }, + ], + unsavedChanges: [ + false, + { + setSearchSettings: () => true, + }, + ], + + dataLoading: [ + true, + { + onInitializeRelevanceTuning: () => false, + resetSearchSettingsState: () => true, + }, + ], + searchResults: [ + null, + { + clearSearchResults: () => null, + setSearchResults: (_, searchResults) => searchResults, + }, + ], + resultsLoading: [ + false, + { + setResultsLoading: (_, resultsLoading) => resultsLoading, + setSearchResults: () => false, + }, + ], + }), + selectors: ({ selectors }) => ({ + schemaFields: [() => [selectors.schema], (schema: Schema) => Object.keys(schema)], + schemaFieldsWithConflicts: [ + () => [selectors.schemaConflicts], + (schemaConflicts: SchemaConflicts) => Object.keys(schemaConflicts), + ], + filteredSchemaFields: [ + () => [selectors.schemaFields, selectors.filterInputValue], + (schemaFields: string[], filterInputValue: string): string[] => + filterIfTerm(schemaFields, filterInputValue), + ], + filteredSchemaFieldsWithConflicts: [ + () => [selectors.schemaFieldsWithConflicts, selectors.filterInputValue], + (schemaFieldsWithConflicts: string[], filterInputValue: string): string[] => + filterIfTerm(schemaFieldsWithConflicts, filterInputValue), + ], + engineHasSchemaFields: [ + () => [selectors.schema], + (schema: Schema): boolean => Object.keys(schema).length >= 2, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts new file mode 100644 index 00000000000000..25187df89d64c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type BoostType = 'value' | 'functional' | 'proximity'; + +export interface Boost { + type: BoostType; + operation?: string; + function?: string; + newBoost?: boolean; + center?: string | number; + value?: string | number | string[] | number[]; + factor: number; +} + +export interface SearchSettings { + boosts: Record; + search_fields: object; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index dbde764a568614..2fa00c7f029f1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -48,30 +48,31 @@ import { ViewContentHeader } from '../../../components/shared/view_content_heade import { SourceDataItem } from '../../../types'; import { AppLogic } from '../../../app_logic'; +import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { staticSourceData } from '../source_data'; import { SourceLogic } from '../source_logic'; export const SourceSettings: React.FC = () => { - const { - updateContentSource, - removeContentSource, - resetSourceState, - getSourceConfigData, - } = useActions(SourceLogic); + const { updateContentSource, removeContentSource, resetSourceState } = useActions(SourceLogic); + const { getSourceConfigData } = useActions(AddSourceLogic); const { contentSource: { name, id, serviceType }, buttonLoading, - sourceConfigData: { configuredFields }, } = useValues(SourceLogic); + const { + sourceConfigData: { configuredFields }, + } = useValues(AddSourceLogic); + const { isOrganization } = useValues(AppLogic); useEffect(() => { getSourceConfigData(serviceType); return resetSourceState; }, []); + const { configuration: { isPublicKey }, editPath, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index bf5ec5a949b8d2..15df7ddc993954 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -18,11 +18,7 @@ jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); -import { - fullContentSources, - sourceConfigData, - contentItems, -} from '../../__mocks__/content_sources.mock'; +import { fullContentSources, contentItems } from '../../__mocks__/content_sources.mock'; import { meta } from '../../__mocks__/meta.mock'; import { DEFAULT_META } from '../../../shared/constants'; @@ -46,7 +42,6 @@ describe('SourceLogic', () => { const defaultValues = { contentSource: {}, contentItems: [], - sourceConfigData: {}, dataLoading: true, sectionLoading: true, buttonLoading: false, @@ -88,13 +83,6 @@ describe('SourceLogic', () => { expect(setSuccessMessage).toHaveBeenCalled(); }); - it('setSourceConfigData', () => { - SourceLogic.actions.setSourceConfigData(sourceConfigData); - - expect(SourceLogic.values.sourceConfigData).toEqual(sourceConfigData); - expect(SourceLogic.values.dataLoading).toEqual(false); - }); - it('setSearchResults', () => { SourceLogic.actions.setSearchResults(searchServerResponse); @@ -402,40 +390,6 @@ describe('SourceLogic', () => { }); }); - describe('getSourceConfigData', () => { - const serviceType = 'github'; - - it('calls API and sets values', async () => { - AppLogic.values.isOrganization = true; - - const setSourceConfigDataSpy = jest.spyOn(SourceLogic.actions, 'setSourceConfigData'); - const promise = Promise.resolve(contentSource); - http.get.mockReturnValue(promise); - SourceLogic.actions.getSourceConfigData(serviceType); - - expect(http.get).toHaveBeenCalledWith( - `/api/workplace_search/org/settings/connectors/${serviceType}` - ); - await promise; - expect(setSourceConfigDataSpy).toHaveBeenCalled(); - }); - - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); - SourceLogic.actions.getSourceConfigData(serviceType); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); - }); - }); - it('resetSourceState', () => { SourceLogic.actions.resetSourceState(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 1eef715350848e..c1f5d6033543fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -27,7 +27,6 @@ import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } f export interface SourceActions { onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData; onUpdateSourceName(name: string): string; - setSourceConfigData(sourceConfigData: SourceConfigData): SourceConfigData; setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse; initializeFederatedSummary(sourceId: string): { sourceId: string }; onUpdateSummary(summary: DocumentSummaryItem[]): DocumentSummaryItem[]; @@ -41,28 +40,9 @@ export interface SourceActions { resetSourceState(): void; removeContentSource(sourceId: string): { sourceId: string }; initializeSource(sourceId: string): { sourceId: string }; - getSourceConfigData(serviceType: string): { serviceType: string }; setButtonNotLoading(): void; } -interface SourceConfigData { - serviceType: string; - name: string; - configured: boolean; - categories: string[]; - needsPermissions?: boolean; - privateSourcesEnabled: boolean; - configuredFields: { - publicKey: string; - privateKey: string; - consumerKey: string; - baseUrl?: string; - clientId?: string; - clientSecret?: string; - }; - accountContextOnly?: boolean; -} - interface SourceValues { contentSource: ContentSourceFullData; dataLoading: boolean; @@ -71,7 +51,6 @@ interface SourceValues { contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; - sourceConfigData: SourceConfigData; } interface SearchResultsResponse { @@ -84,7 +63,6 @@ export const SourceLogic = kea>({ actions: { onInitializeSource: (contentSource: ContentSourceFullData) => contentSource, onUpdateSourceName: (name: string) => name, - setSourceConfigData: (sourceConfigData: SourceConfigData) => sourceConfigData, onUpdateSummary: (summary: object[]) => summary, setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse, setContentFilterValue: (contentFilterValue: string) => contentFilterValue, @@ -96,7 +74,6 @@ export const SourceLogic = kea>({ removeContentSource: (sourceId: string) => ({ sourceId, }), - getSourceConfigData: (serviceType: string) => ({ serviceType }), resetSourceState: () => true, setButtonNotLoading: () => false, }, @@ -115,17 +92,10 @@ export const SourceLogic = kea>({ }), }, ], - sourceConfigData: [ - {} as SourceConfigData, - { - setSourceConfigData: (_, sourceConfigData) => sourceConfigData, - }, - ], dataLoading: [ true, { onInitializeSource: () => false, - setSourceConfigData: () => false, resetSourceState: () => false, }, ], @@ -133,7 +103,6 @@ export const SourceLogic = kea>({ false, { setButtonNotLoading: () => false, - setSourceConfigData: () => false, resetSourceState: () => false, removeContentSource: () => true, }, @@ -181,7 +150,6 @@ export const SourceLogic = kea>({ actions.initializeFederatedSummary(sourceId); } } catch (e) { - // TODO: Verify this works once components are there. Not sure if the catch gives a status code. if (e.response.status === 404) { KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH); } else { @@ -260,16 +228,6 @@ export const SourceLogic = kea>({ actions.setButtonNotLoading(); } }, - getSourceConfigData: async ({ serviceType }) => { - const route = `/api/workplace_search/org/settings/connectors/${serviceType}`; - - try { - const response = await HttpLogic.values.http.get(route); - actions.setSourceConfigData(response); - } catch (e) { - flashAPIErrors(e); - } - }, onUpdateSourceName: (name: string) => { setSuccessMessage( i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index 79f418a48dabc5..4b59e0f3401c56 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -15,7 +15,6 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { Loading } from '../../../../shared/loading'; import { SourceDataItem } from '../../../types'; import { staticSourceData } from '../../content_sources/source_data'; -import { SourceLogic } from '../../content_sources/source_logic'; import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header'; @@ -31,18 +30,18 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => { const [confirmModalVisible, setConfirmModalVisibility] = useState(false); const { configuration, serviceType } = staticSourceData[sourceIndex] as SourceDataItem; const { deleteSourceConfig } = useActions(SettingsLogic); - const { getSourceConfigData } = useActions(SourceLogic); - const { saveSourceConfig } = useActions(AddSourceLogic); + const { saveSourceConfig, getSourceConfigData } = useActions(AddSourceLogic); const { sourceConfigData: { name, categories }, - dataLoading: sourceDataLoading, - } = useValues(SourceLogic); + dataLoading, + } = useValues(AddSourceLogic); useEffect(() => { getSourceConfigData(serviceType); }, []); - if (sourceDataLoading) return ; + if (dataLoading) return ; + const hideConfirmModal = () => setConfirmModalVisibility(false); const showConfirmModal = () => setConfirmModalVisibility(true); const saveUpdatedConfig = () => saveSourceConfig(true); diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts index 7fde7934cf7ad1..88cf30bb2a5492 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts @@ -23,7 +23,6 @@ type PayloadType = 'params' | 'query' | 'body'; interface IMockRouter { method: MethodType; path: string; - payload?: PayloadType; } interface IMockRouterRequest { body?: object; @@ -39,11 +38,10 @@ export class MockRouter { public payload?: PayloadType; public response = httpServerMock.createResponseFactory(); - constructor({ method, path, payload }: IMockRouter) { + constructor({ method, path }: IMockRouter) { this.createRouter(); this.method = method; this.path = path; - this.payload = payload; } public createRouter = () => { @@ -62,16 +60,17 @@ export class MockRouter { */ public validateRoute = (request: MockRouterRequest) => { - if (!this.payload) throw new Error('Cannot validate wihout a payload type specified.'); - const route = this.findRouteRegistration(); const [config] = route; const validate = config.validate as RouteValidatorConfig<{}, {}, {}>; + const payloads = Object.keys(request) as PayloadType[]; - const payloadValidation = validate[this.payload] as { validate(request: KibanaRequest): void }; - const payloadRequest = request[this.payload] as KibanaRequest; + payloads.forEach((payload: PayloadType) => { + const payloadValidation = validate[payload] as { validate(request: KibanaRequest): void }; + const payloadRequest = request[payload] as KibanaRequest; - payloadValidation.validate(payloadRequest); + payloadValidation.validate(payloadRequest); + }); }; public shouldValidate = (request: MockRouterRequest) => { @@ -99,7 +98,6 @@ export class MockRouter { // const mockRouter = new MockRouter({ // method: 'get', // path: '/api/app_search/test', -// payload: 'body' // }); // // beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts index 3d63e4044e75bc..8e4a7dba165b10 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts @@ -18,7 +18,6 @@ describe('analytics routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/app_search/engines/{engineName}/analytics/queries', - payload: 'query', }); registerAnalyticsRoutes({ @@ -71,7 +70,6 @@ describe('analytics routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/app_search/engines/{engineName}/analytics/queries/{query}', - payload: 'query', }); registerAnalyticsRoutes({ diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts index 7a513b1c76b4e7..d9e84d3e62f281 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -18,7 +18,6 @@ describe('credentials routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/app_search/credentials', - payload: 'query', }); registerCredentialsRoutes({ @@ -54,7 +53,6 @@ describe('credentials routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/app_search/credentials', - payload: 'body', }); registerCredentialsRoutes({ @@ -167,7 +165,6 @@ describe('credentials routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/app_search/credentials/details', - payload: 'query', }); registerCredentialsRoutes({ @@ -191,7 +188,6 @@ describe('credentials routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/app_search/credentials/{name}', - payload: 'body', }); registerCredentialsRoutes({ diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts index fdae51444bb54e..af54d340ad150b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts @@ -18,7 +18,6 @@ describe('documents routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/app_search/engines/{engineName}/documents', - payload: 'body', }); registerDocumentsRoutes({ diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index e874a188a10f7d..abd26e18c7b9d8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -29,7 +29,6 @@ describe('engine routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/app_search/engines', - payload: 'query', }); registerEnginesRoutes({ diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts index 92a695af12aaab..d8f677e2f0d826 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts @@ -87,7 +87,6 @@ describe('search settings routes', () => { const mockRouter = new MockRouter({ method: 'put', path: '/api/app_search/engines/{engineName}/search_settings', - payload: 'body', }); beforeEach(() => { @@ -149,7 +148,6 @@ describe('search settings routes', () => { const mockRouter = new MockRouter({ method: 'post', path: '/api/app_search/engines/{engineName}/search_settings_search', - payload: 'body', }); beforeEach(() => { @@ -188,29 +186,18 @@ describe('search settings routes', () => { }); describe('validates query', () => { - const queryRouter = new MockRouter({ - method: 'post', - path: '/api/app_search/engines/{engineName}/search_settings_search', - payload: 'query', - }); - it('correctly', () => { - registerSearchSettingsRoutes({ - ...mockDependencies, - router: queryRouter.router, - }); - const request = { query: { query: 'foo', }, }; - queryRouter.shouldValidate(request); + mockRouter.shouldValidate(request); }); it('missing required fields', () => { const request = { query: {} }; - queryRouter.shouldThrow(request); + mockRouter.shouldThrow(request); }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts index 5d56bbf4fcd11a..6df9a4f16d7106 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts @@ -41,7 +41,6 @@ describe('log settings routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/app_search/log_settings', - payload: 'body', }); registerSettingsRoutes({ diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index f41ad367839c3e..08c398ba3eb0d9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -29,7 +29,6 @@ describe('Enterprise Search Telemetry API', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/enterprise_search/stats', - payload: 'body', }); registerTelemetryRoute({ diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts index e67ca4c064886f..68a9ae725f8a47 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts @@ -26,7 +26,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/workplace_search/groups', - payload: 'query', }); registerGroupsRoute({ @@ -50,7 +49,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/groups', - payload: 'body', }); registerGroupsRoute({ @@ -85,7 +83,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/groups/search', - payload: 'body', }); registerSearchGroupsRoute({ @@ -163,7 +160,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/groups/{id}', - payload: 'body', }); registerGroupRoute({ @@ -246,7 +242,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/groups/{id}/share', - payload: 'body', }); registerShareGroupRoute({ @@ -282,7 +277,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/groups/{id}/assign', - payload: 'body', }); registerAssignGroupRoute({ @@ -318,7 +312,6 @@ describe('groups routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/groups/{id}/boosts', - payload: 'body', }); registerBoostsGroupRoute({ diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts index 1afb85b389b427..bdf885648dff7f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts @@ -18,7 +18,6 @@ describe('Overview route', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/workplace_search/overview', - payload: 'query', }); registerOverviewRoute({ diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts index f2117a8bc948a6..a1615499c56a29 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts @@ -45,7 +45,6 @@ describe('security routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/workplace_search/org/security/source_restrictions', - payload: 'body', }); registerSecuritySourceRestrictionsRoute({ @@ -72,7 +71,6 @@ describe('security routes', () => { mockRouter = new MockRouter({ method: 'patch', path: '/api/workplace_search/org/security/source_restrictions', - payload: 'body', }); registerSecuritySourceRestrictionsRoute({ diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts index cf654918beb49b..00a5b6c75df0a5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts @@ -45,7 +45,6 @@ describe('settings routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/org/settings/customize', - payload: 'body', }); registerOrgSettingsCustomizeRoute({ @@ -76,7 +75,6 @@ describe('settings routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/org/settings/oauth_application', - payload: 'body', }); registerOrgSettingsOauthApplicationRoute({ diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 2ae10e85ea9c0c..a2fbe759f1a115 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -154,7 +154,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/account/create_source', - payload: 'body', }); registerAccountCreateSourceRoute({ @@ -194,7 +193,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/account/sources/{id}/documents', - payload: 'body', }); registerAccountSourceDocumentsRoute({ @@ -281,7 +279,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'patch', path: '/api/workplace_search/account/sources/{id}/settings', - payload: 'body', }); registerAccountSourceSettingsRoute({ @@ -364,7 +361,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/account/sources/{id}/searchable', - payload: 'body', }); registerAccountSourceSearchableRoute({ @@ -422,7 +418,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/account/sources/{id}/display_settings/config', - payload: 'body', }); registerAccountSourceDisplaySettingsConfig({ @@ -489,7 +484,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/account/sources/{id}/schemas', - payload: 'body', }); registerAccountSourceSchemasRoute({ @@ -667,7 +661,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/org/create_source', - payload: 'body', }); registerOrgCreateSourceRoute({ @@ -707,7 +700,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/org/sources/{id}/documents', - payload: 'body', }); registerOrgSourceDocumentsRoute({ @@ -794,7 +786,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'patch', path: '/api/workplace_search/org/sources/{id}/settings', - payload: 'body', }); registerOrgSourceSettingsRoute({ @@ -877,7 +868,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/org/sources/{id}/searchable', - payload: 'body', }); registerOrgSourceSearchableRoute({ @@ -935,7 +925,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/org/sources/{id}/display_settings/config', - payload: 'body', }); registerOrgSourceDisplaySettingsConfig({ @@ -1002,7 +991,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/org/sources/{id}/schemas', - payload: 'body', }); registerOrgSourceSchemasRoute({ @@ -1102,7 +1090,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/org/settings/connectors', - payload: 'body', }); registerOrgSourceOauthConfigurationsRoute({ @@ -1133,7 +1120,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/org/settings/connectors', - payload: 'body', }); registerOrgSourceOauthConfigurationsRoute({ @@ -1187,7 +1173,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'post', path: '/api/workplace_search/org/settings/connectors/{serviceType}', - payload: 'body', }); registerOrgSourceOauthConfigurationRoute({ @@ -1218,7 +1203,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'put', path: '/api/workplace_search/org/settings/connectors/{serviceType}', - payload: 'body', }); registerOrgSourceOauthConfigurationRoute({ @@ -1272,7 +1256,6 @@ describe('sources routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/workplace_search/sources/create', - payload: 'query', }); registerOauthConnectorParamsRoute({ diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index 363607aae2b467..96b6249585bfcd 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -24,6 +24,7 @@ export const DEFAULT_AGENT_POLICY: Omit< status: agentPolicyStatuses.Active, package_policies: [], is_default: true, + is_managed: false, monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 5e86e8e6acb70c..5f41b0f70ca74f 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -17,6 +17,7 @@ export interface NewAgentPolicy { namespace: string; description?: string; is_default?: boolean; + is_managed?: boolean; // Optional when creating a policy monitoring_enabled?: Array>; } @@ -24,6 +25,7 @@ export interface AgentPolicy extends NewAgentPolicy { id: string; status: ValueOf; package_policies: string[] | PackagePolicy[]; + is_managed: boolean; // required for created policy updated_at: string; updated_by: string; revision: number; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 30588c10178da6..b60d3b5eb1f2d1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -687,6 +687,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos 'e8a37031-2907-44f6-89d2-98bd493f60dc', ], is_default: true, + is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 6, updated_at: '2020-12-09T13:46:31.840Z', @@ -701,6 +702,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos status: 'active', package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'], is_default: false, + is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 2, updated_at: '2020-12-09T13:46:31.840Z', diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index ee30c01ac8eece..a903de01380392 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -32,3 +32,5 @@ export class PackageCacheError extends IngestManagerError {} export class PackageOperationNotSupportedError extends IngestManagerError {} export class FleetAdminUserInvalidError extends IngestManagerError {} export class ConcurrentInstallOperationError extends IngestManagerError {} +export class AgentReassignmentError extends IngestManagerError {} +export class AgentUnenrollmentError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts index 0365d9f5a29fe5..614ccd8a266245 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -24,7 +24,7 @@ export const postAgentUnenrollHandler: RequestHandler< if (request.body?.force === true) { await AgentService.forceUnenrollAgent(soClient, esClient, request.params.agentId); } else { - await AgentService.unenrollAgent(soClient, request.params.agentId); + await AgentService.unenrollAgent(soClient, esClient, request.params.agentId); } const body: PostAgentUnenrollResponse = {}; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index c61dd1b8e4a194..d50db8d9809f4d 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -32,7 +32,7 @@ import { migrateSettingsToV7100, migrateAgentActionToV7100, } from './migrations/to_v7_10_0'; -import { migrateAgentToV7120 } from './migrations/to_v7_12_0'; +import { migrateAgentToV7120, migrateAgentPolicyToV7120 } from './migrations/to_v7_12_0'; /* * Saved object types and mappings @@ -161,6 +161,7 @@ const getSavedObjectTypes = ( description: { type: 'text' }, namespace: { type: 'keyword' }, is_default: { type: 'boolean' }, + is_managed: { type: 'boolean' }, status: { type: 'keyword' }, package_policies: { type: 'keyword' }, updated_at: { type: 'date' }, @@ -171,6 +172,7 @@ const getSavedObjectTypes = ( }, migrations: { '7.10.0': migrateAgentPolicyToV7100, + '7.12.0': migrateAgentPolicyToV7120, }, }, [ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts index 1635f38cd55220..49a0d6fc7737fa 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { SavedObjectMigrationFn } from 'kibana/server'; -import { Agent } from '../../types'; +import type { SavedObjectMigrationFn } from 'kibana/server'; +import type { Agent, AgentPolicy } from '../../types'; export const migrateAgentToV7120: SavedObjectMigrationFn = ( agentDoc @@ -15,3 +15,14 @@ export const migrateAgentToV7120: SavedObjectMigrationFn, + AgentPolicy +> = (agentPolicyDoc) => { + const isV12 = 'is_managed' in agentPolicyDoc.attributes; + if (!isV12) { + agentPolicyDoc.attributes.is_managed = false; + } + return agentPolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index b70041e66dcd9a..800d4f479bfde2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -8,17 +8,16 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { agentPolicyService } from './agent_policy'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; -import { Output } from '../types'; +import type { AgentPolicy, NewAgentPolicy, Output } from '../types'; function getSavedObjectMock(agentPolicyAttributes: any) { const mock = savedObjectsClientMock.create(); - mock.get.mockImplementation(async (type: string, id: string) => { return { type, id, references: [], - attributes: agentPolicyAttributes, + attributes: agentPolicyAttributes as AgentPolicy, }; }); mock.find.mockImplementation(async (options) => { @@ -69,10 +68,59 @@ function getAgentPolicyUpdateMock() { >; } +function getAgentPolicyCreateMock() { + const soClient = savedObjectsClientMock.create(); + soClient.create.mockImplementation(async (type, attributes) => { + return { + attributes: (attributes as unknown) as NewAgentPolicy, + id: 'mocked', + type: 'mocked', + references: [], + }; + }); + return soClient; +} describe('agent policy', () => { beforeEach(() => { getAgentPolicyUpdateMock().mockClear(); }); + + describe('create', () => { + it('is_managed present and false by default', async () => { + // ignore unrelated unique name constraint + agentPolicyService.requireUniqueName = async () => {}; + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + await expect( + agentPolicyService.create(soClient, esClient, { + name: 'No is_managed provided', + namespace: 'default', + }) + ).resolves.toHaveProperty('is_managed', false); + + const [, attributes] = soClient.create.mock.calls[0]; + expect(attributes).toHaveProperty('is_managed', false); + }); + + it('should set is_managed property, if given', async () => { + // ignore unrelated unique name constraint + agentPolicyService.requireUniqueName = async () => {}; + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await expect( + agentPolicyService.create(soClient, esClient, { + name: 'is_managed: true provided', + namespace: 'default', + is_managed: true, + }) + ).resolves.toHaveProperty('is_managed', true); + + const [, attributes] = soClient.create.mock.calls[0]; + expect(attributes).toHaveProperty('is_managed', true); + }); + }); + describe('bumpRevision', () => { it('should call agentPolicyUpdateEventHandler with updated event once', async () => { const soClient = getSavedObjectMock({ @@ -208,4 +256,37 @@ describe('agent policy', () => { }); }); }); + + describe('update', () => { + it('should update is_managed property, if given', async () => { + // ignore unrelated unique name constraint + agentPolicyService.requireUniqueName = async () => {}; + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'mocked', + type: 'mocked', + references: [], + }); + await agentPolicyService.update(soClient, esClient, 'mocked', { + name: 'mocked', + namespace: 'default', + is_managed: false, + }); + // soClient.update is called with updated values + let calledWith = soClient.update.mock.calls[0]; + expect(calledWith[2]).toHaveProperty('is_managed', false); + + await agentPolicyService.update(soClient, esClient, 'mocked', { + name: 'is_managed: true provided', + namespace: 'default', + is_managed: true, + }); + // soClient.update is called with updated values + calledWith = soClient.update.mock.calls[1]; + expect(calledWith[2]).toHaveProperty('is_managed', true); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 4a3319941b5757..dfe5c19bc417b5 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -140,6 +140,7 @@ class AgentPolicyService { SAVED_OBJECT_TYPE, { ...agentPolicy, + is_managed: agentPolicy.is_managed ?? false, revision: 1, updated_at: new Date().toISOString(), updated_by: options?.user?.username || 'system', diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 9382a8bb616479..36506d05905958 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -12,7 +12,7 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { escapeSearchQueryPhrase } from '../saved_object'; import { savedObjectToAgent } from './saved_objects'; -import { appContextService } from '../../services'; +import { appContextService, agentPolicyService } from '../../services'; import * as crudServiceSO from './crud_so'; import * as crudServiceFleetServer from './crud_fleet_server'; @@ -86,6 +86,22 @@ export async function getAgents(soClient: SavedObjectsClientContract, agentIds: return agents; } +export async function getAgentPolicyForAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const agent = await getAgent(soClient, esClient, agentId); + if (!agent.policy_id) { + return; + } + + const agentPolicy = await agentPolicyService.get(soClient, agent.policy_id, false); + if (agentPolicy) { + return agentPolicy; + } +} + export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, accessAPIKeyId: string diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts new file mode 100644 index 00000000000000..7338c440483ea6 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import type { SavedObject } from 'kibana/server'; +import type { Agent, AgentPolicy } from '../../types'; +import { AgentReassignmentError } from '../../errors'; +import { reassignAgent, reassignAgents } from './reassign'; + +const agentInManagedSO = { + id: 'agent-in-managed-policy', + attributes: { policy_id: 'managed-agent-policy' }, +} as SavedObject; +const agentInManagedSO2 = { + id: 'agent-in-managed-policy2', + attributes: { policy_id: 'managed-agent-policy' }, +} as SavedObject; +const agentInUnmanagedSO = { + id: 'agent-in-unmanaged-policy', + attributes: { policy_id: 'unmanaged-agent-policy' }, +} as SavedObject; +const agentInUnmanagedSO2 = { + id: 'agent-in-unmanaged-policy2', + attributes: { policy_id: 'unmanaged-agent-policy' }, +} as SavedObject; +const unmanagedAgentPolicySO = { + id: 'unmanaged-agent-policy', + attributes: { is_managed: false }, +} as SavedObject; +const managedAgentPolicySO = { + id: 'managed-agent-policy', + attributes: { is_managed: true }, +} as SavedObject; + +describe('reassignAgent (singular)', () => { + it('can reassign from unmanaged policy to unmanaged', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await reassignAgent(soClient, esClient, agentInUnmanagedSO.id, agentInUnmanagedSO2.id); + + // calls ES update with correct values + expect(soClient.update).toBeCalledTimes(1); + const calledWith = soClient.update.mock.calls[0]; + expect(calledWith[1]).toBe(agentInUnmanagedSO.id); + expect(calledWith[2]).toHaveProperty('policy_id', agentInUnmanagedSO2.id); + }); + + it('cannot reassign from unmanaged policy to managed', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await expect( + reassignAgent( + soClient, + esClient, + agentInUnmanagedSO.id, + agentInManagedSO.attributes.policy_id! + ) + ).rejects.toThrowError(AgentReassignmentError); + + // does not call ES update + expect(soClient.update).toBeCalledTimes(0); + }); + + it('cannot reassign from managed policy', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await expect( + reassignAgent(soClient, esClient, agentInManagedSO.id, agentInManagedSO2.id) + ).rejects.toThrowError(AgentReassignmentError); + // does not call ES update + expect(soClient.update).toBeCalledTimes(0); + + await expect( + reassignAgent(soClient, esClient, agentInManagedSO.id, agentInUnmanagedSO.id) + ).rejects.toThrowError(AgentReassignmentError); + // does not call ES update + expect(soClient.update).toBeCalledTimes(0); + }); +}); + +describe('reassignAgents (plural)', () => { + it('agents in managed policies are not updated', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const idsToReassign = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO.id]; + await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, agentInUnmanagedSO.id); + + // calls ES update with correct values + const calledWith = soClient.bulkUpdate.mock.calls[0][0]; + const expectedResults = [agentInUnmanagedSO.id, agentInUnmanagedSO.id]; + expect(calledWith.length).toBe(expectedResults.length); // only 2 are unmanaged + expect(calledWith.map(({ id }) => id)).toEqual(expectedResults); + }); +}); + +function createClientMock() { + const soClientMock = savedObjectsClientMock.create(); + + // need to mock .create & bulkCreate due to (bulk)createAgentAction(s) in reassignAgent(s) + soClientMock.create.mockResolvedValue(agentInUnmanagedSO); + soClientMock.bulkCreate.mockImplementation(async ([{ type, attributes }]) => { + return { + saved_objects: [await soClientMock.create(type, attributes)], + }; + }); + + soClientMock.get.mockImplementation(async (_, id) => { + switch (id) { + case unmanagedAgentPolicySO.id: + return unmanagedAgentPolicySO; + case managedAgentPolicySO.id: + return managedAgentPolicySO; + case agentInManagedSO.id: + return agentInManagedSO; + case agentInUnmanagedSO.id: + default: + return agentInUnmanagedSO; + } + }); + + soClientMock.bulkGet.mockImplementation(async (options) => { + return { + saved_objects: await Promise.all(options!.map(({ type, id }) => soClientMock.get(type, id))), + }; + }); + + return soClientMock; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index fbd91c05dfb4ac..9f4373ab553ecf 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; +import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { AgentSOAttributes } from '../../types'; +import type { AgentSOAttributes } from '../../types'; +import { AgentReassignmentError } from '../../errors'; import { agentPolicyService } from '../agent_policy'; -import { getAgents, listAllAgents } from './crud'; +import { getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( @@ -19,11 +20,13 @@ export async function reassignAgent( agentId: string, newAgentPolicyId: string ) { - const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); - if (!agentPolicy) { + const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + if (!newAgentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); } + await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { policy_id: newAgentPolicyId, policy_revision: null, @@ -36,6 +39,29 @@ export async function reassignAgent( }); } +export async function reassignAgentIsAllowed( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string, + newAgentPolicyId: string +) { + const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); + if (agentPolicy?.is_managed) { + throw new AgentReassignmentError( + `Cannot reassign an agent from managed agent policy ${agentPolicy.id}` + ); + } + + const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + if (newAgentPolicy?.is_managed) { + throw new AgentReassignmentError( + `Cannot reassign an agent to managed agent policy ${newAgentPolicy.id}` + ); + } + + return true; +} + export async function reassignAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -63,7 +89,15 @@ export async function reassignAgents( showInactive: false, }) ).agents; - const agentsToUpdate = agents.filter((agent) => agent.policy_id !== newAgentPolicyId); + // And which are allowed to unenroll + const settled = await Promise.allSettled( + agents.map((agent) => + reassignAgentIsAllowed(soClient, esClient, agent.id, newAgentPolicyId).then((_) => agent) + ) + ); + const agentsToUpdate = agents.filter( + (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId + ); // Update the necessary agents const res = await soClient.bulkUpdate( diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts new file mode 100644 index 00000000000000..b8c1b7befb443c --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import type { SavedObject } from 'kibana/server'; +import type { Agent, AgentPolicy } from '../../types'; +import { AgentUnenrollmentError } from '../../errors'; +import { unenrollAgent, unenrollAgents } from './unenroll'; + +const agentInManagedSO = { + id: 'agent-in-managed-policy', + attributes: { policy_id: 'managed-agent-policy' }, +} as SavedObject; +const agentInUnmanagedSO = { + id: 'agent-in-unmanaged-policy', + attributes: { policy_id: 'unmanaged-agent-policy' }, +} as SavedObject; +const agentInUnmanagedSO2 = { + id: 'agent-in-unmanaged-policy2', + attributes: { policy_id: 'unmanaged-agent-policy' }, +} as SavedObject; +const unmanagedAgentPolicySO = { + id: 'unmanaged-agent-policy', + attributes: { is_managed: false }, +} as SavedObject; +const managedAgentPolicySO = { + id: 'managed-agent-policy', + attributes: { is_managed: true }, +} as SavedObject; + +describe('unenrollAgent (singular)', () => { + it('can unenroll from unmanaged policy', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await unenrollAgent(soClient, esClient, agentInUnmanagedSO.id); + + // calls ES update with correct values + expect(soClient.update).toBeCalledTimes(1); + const calledWith = soClient.update.mock.calls[0]; + expect(calledWith[1]).toBe(agentInUnmanagedSO.id); + expect(calledWith[2]).toHaveProperty('unenrollment_started_at'); + }); + + it('cannot unenroll from managed policy', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await expect(unenrollAgent(soClient, esClient, agentInManagedSO.id)).rejects.toThrowError( + AgentUnenrollmentError + ); + // does not call ES update + expect(soClient.update).toBeCalledTimes(0); + }); +}); + +describe('unenrollAgents (plural)', () => { + it('can unenroll from an unmanaged policy', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const idsToUnenroll = [agentInUnmanagedSO.id, agentInUnmanagedSO2.id]; + await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll }); + + // calls ES update with correct values + const calledWith = soClient.bulkUpdate.mock.calls[0][0]; + expect(calledWith.length).toBe(idsToUnenroll.length); + expect(calledWith.map(({ id }) => id)).toEqual(idsToUnenroll); + for (const params of calledWith) { + expect(params.attributes).toHaveProperty('unenrollment_started_at'); + } + }); + it('cannot unenroll from a managed policy', async () => { + const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const idsToUnenroll = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO2.id]; + await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll }); + + // calls ES update with correct values + const calledWith = soClient.bulkUpdate.mock.calls[0][0]; + const onlyUnmanaged = [agentInUnmanagedSO.id, agentInUnmanagedSO2.id]; + expect(calledWith.length).toBe(onlyUnmanaged.length); + expect(calledWith.map(({ id }) => id)).toEqual(onlyUnmanaged); + for (const params of calledWith) { + expect(params.attributes).toHaveProperty('unenrollment_started_at'); + } + }); +}); + +function createClientMock() { + const soClientMock = savedObjectsClientMock.create(); + + // need to mock .create & bulkCreate due to (bulk)createAgentAction(s) in unenrollAgent(s) + soClientMock.create.mockResolvedValue(agentInUnmanagedSO); + soClientMock.bulkCreate.mockImplementation(async ([{ type, attributes }]) => { + return { + saved_objects: [await soClientMock.create(type, attributes)], + }; + }); + + soClientMock.get.mockImplementation(async (_, id) => { + switch (id) { + case unmanagedAgentPolicySO.id: + return unmanagedAgentPolicySO; + case managedAgentPolicySO.id: + return managedAgentPolicySO; + case agentInManagedSO.id: + return agentInManagedSO; + case agentInUnmanagedSO2.id: + return agentInUnmanagedSO2; + case agentInUnmanagedSO.id: + default: + return agentInUnmanagedSO; + } + }); + + soClientMock.bulkGet.mockImplementation(async (options) => { + return { + saved_objects: await Promise.all(options!.map(({ type, id }) => soClientMock.get(type, id))), + }; + }); + + return soClientMock; +} diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index a20b742d1425e6..e2fa83cf32b637 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -4,16 +4,36 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { AgentSOAttributes } from '../../types'; +import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { AgentSOAttributes } from '../../types'; +import { AgentUnenrollmentError } from '../../errors'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; import { createAgentAction, bulkCreateAgentActions } from './actions'; -import { getAgents, listAllAgents } from './crud'; +import { getAgent, getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; + +async function unenrollAgentIsAllowed( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); + if (agentPolicy?.is_managed) { + throw new AgentUnenrollmentError( + `Cannot unenroll ${agentId} from a managed agent policy ${agentPolicy.id}` + ); + } + + return true; +} + +export async function unenrollAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + await unenrollAgentIsAllowed(soClient, esClient, agentId); -export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const now = new Date().toISOString(); await createAgentAction(soClient, { agent_id: agentId, @@ -36,7 +56,6 @@ export async function unenrollAgents( kuery: string; } ) { - // Filter to agents that do not already unenrolled, or unenrolling const agents = 'agentIds' in options ? await getAgents(soClient, options.agentIds) @@ -46,9 +65,19 @@ export async function unenrollAgents( showInactive: false, }) ).agents; - const agentsToUpdate = agents.filter( + + // Filter to agents that are not already unenrolled, or unenrolling + const agentsEnrolled = agents.filter( (agent) => !agent.unenrollment_started_at && !agent.unenrolled_at ); + // And which are allowed to unenroll + const settled = await Promise.allSettled( + agentsEnrolled.map((agent) => + unenrollAgentIsAllowed(soClient, esClient, agent.id).then((_) => agent) + ) + ); + const agentsToUpdate = agentsEnrolled.filter((_, index) => settled[index].status === 'fulfilled'); + const now = new Date().toISOString(); // Create unenroll action for each agent diff --git a/x-pack/plugins/fleet/server/services/agents/update.ts b/x-pack/plugins/fleet/server/services/agents/update.ts index f6b4b440047613..21087be392bcd0 100644 --- a/x-pack/plugins/fleet/server/services/agents/update.ts +++ b/x-pack/plugins/fleet/server/services/agents/update.ts @@ -29,7 +29,7 @@ export async function unenrollForAgentPolicyId( hasMore = false; } for (const agent of agents) { - await unenrollAgent(soClient, agent.id); + await unenrollAgent(soClient, esClient, agent.id); } } } diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index 209bfb4b7398a8..5891320c2544bc 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -13,6 +13,7 @@ const AgentPolicyBaseSchema = { name: schema.string({ minLength: 1 }), namespace: NamespaceSchema, description: schema.maybe(schema.string()), + is_managed: schema.maybe(schema.boolean()), monitoring_enabled: schema.maybe( schema.arrayOf( schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)]) @@ -27,6 +28,7 @@ export const NewAgentPolicySchema = schema.object({ export const AgentPolicySchema = schema.object({ ...AgentPolicyBaseSchema, id: schema.string(), + is_managed: schema.boolean(), status: schema.oneOf([ schema.literal(agentPolicyStatuses.Active), schema.literal(agentPolicyStatuses.Inactive), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 3f842792c20cfa..4e7e07b99904f5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -14,11 +14,8 @@ import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; -import { - operationDefinitionMap, - getErrorMessages, - createMockedReferenceOperation, -} from './operations'; +import { operationDefinitionMap, getErrorMessages } from './operations'; +import { createMockedReferenceOperation } from './operations/mocks'; jest.mock('./loader'); jest.mock('../id_generator'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts index 2677c16c566f58..aa46dd765bd8b8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts @@ -32,5 +32,3 @@ export { DerivativeIndexPatternColumn, MovingAverageIndexPatternColumn, } from './definitions'; - -export { createMockedReferenceOperation } from './mocks'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 5f451339746bb1..79d17a7846b8c9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -137,7 +137,7 @@ export const DatavisualizerSelector: FC = () => { > } @@ -167,7 +167,7 @@ export const DatavisualizerSelector: FC = () => { > } diff --git a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx index 8d889a7a4dc2a0..026f1721471920 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx @@ -19,7 +19,7 @@ export interface EnableAlertResponse { } const showTlsAndEncryptionError = () => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const settingsUrl = Legacy.shims.docLinks.links.alerting.generalSettings; Legacy.shims.toastNotifications.addWarning({ title: toMountPoint( @@ -36,11 +36,7 @@ const showTlsAndEncryptionError = () => { })}

- + {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', { defaultMessage: 'Learn how.', })} @@ -51,7 +47,7 @@ const showTlsAndEncryptionError = () => { }; const showUnableToDisableWatcherClusterAlertsError = () => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const settingsUrl = Legacy.shims.docLinks.links.alerting.generalSettings; Legacy.shims.toastNotifications.addWarning({ title: toMountPoint( @@ -68,11 +64,7 @@ const showUnableToDisableWatcherClusterAlertsError = () => { })}

- + {i18n.translate('xpack.monitoring.healthCheck.unableToDisableWatches.action', { defaultMessage: 'Learn more.', })} diff --git a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap index c925ecd1c98ff8..40541aeaad4c10 100644 --- a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap @@ -13,7 +13,7 @@ exports[`Logs should render a default message 1`] = ` values={ Object { "link": Click here for more information @@ -67,7 +67,7 @@ exports[`Logs should render with a no cluster found reason 1`] = ` values={ Object { "link": setup @@ -92,7 +92,7 @@ exports[`Logs should render with a no index found reason 1`] = ` values={ Object { "link": setup @@ -117,7 +117,7 @@ exports[`Logs should render with a no index pattern found reason 1`] = ` values={ Object { "link": Filebeat @@ -142,7 +142,7 @@ exports[`Logs should render with a no node found reason 1`] = ` values={ Object { "link": setup @@ -167,7 +167,7 @@ exports[`Logs should render with a no structured logs reason 1`] = ` values={ Object { "link": points to JSON logs @@ -195,7 +195,7 @@ exports[`Logs should render with a no type found reason 1`] = ` values={ Object { "link": these directions diff --git a/x-pack/plugins/monitoring/public/components/logs/reason.js b/x-pack/plugins/monitoring/public/components/logs/reason.js index 538c8934cdaeff..512b44c8165b10 100644 --- a/x-pack/plugins/monitoring/public/components/logs/reason.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.js @@ -13,7 +13,9 @@ import { Legacy } from '../../legacy_shims'; import { Monospace } from '../metricbeat_migration/instruction_steps/components/monospace/monospace'; export const Reason = ({ reason }) => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const filebeatUrl = Legacy.shims.docLinks.links.filebeat.installation; + const elasticsearchUrl = Legacy.shims.docLinks.links.filebeat.elasticsearchModule; + const troubleshootUrl = Legacy.shims.docLinks.links.monitoring.troubleshootKibana; let title = i18n.translate('xpack.monitoring.logs.reason.defaultTitle', { defaultMessage: 'No log data found', }); @@ -23,10 +25,7 @@ export const Reason = ({ reason }) => { defaultMessage="We did not find any log data and we are unable to diagnose why. {link}" values={{ link: ( - + { defaultMessage="Set up {link}, then configure your Elasticsearch output to your monitoring cluster." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.reason.noIndexPatternLink', { defaultMessage: 'Filebeat', })} @@ -82,10 +78,7 @@ export const Reason = ({ reason }) => { defaultMessage="Follow {link} to set up Elasticsearch." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.reason.noTypeLink', { defaultMessage: 'these directions', })} @@ -105,10 +98,7 @@ export const Reason = ({ reason }) => { values={{ varPaths: var.paths, link: ( - + {i18n.translate('xpack.monitoring.logs.reason.notUsingStructuredLogsLink', { defaultMessage: 'points to JSON logs', })} @@ -127,10 +117,7 @@ export const Reason = ({ reason }) => { defaultMessage="Check that your {link} is correct." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.reason.noClusterLink', { defaultMessage: 'setup', })} @@ -149,10 +136,7 @@ export const Reason = ({ reason }) => { defaultMessage="Check that your {link} is correct." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.reason.noNodeLink', { defaultMessage: 'setup', })} @@ -171,10 +155,7 @@ export const Reason = ({ reason }) => { defaultMessage="We found logs, but none for this index. If this problem continues, check that your {link} is correct." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.reason.noIndexLink', { defaultMessage: 'setup', })} @@ -193,10 +174,7 @@ export const Reason = ({ reason }) => { defaultMessage="There is an issue reading from your filebeat indices. {link}." values={{ link: ( - + {i18n.translate('xpack.monitoring.logs.reason.correctIndexNameLink', { defaultMessage: 'Click here for more information', })} diff --git a/x-pack/plugins/monitoring/public/components/logs/reason.test.js b/x-pack/plugins/monitoring/public/components/logs/reason.test.js index 53aad5511e0ae3..0d75af1d1048fb 100644 --- a/x-pack/plugins/monitoring/public/components/logs/reason.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.test.js @@ -15,6 +15,15 @@ jest.mock('../../legacy_shims', () => ({ docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: 'current', + links: { + filebeat: { + elasticsearchModule: 'jest-metadata-mock-url', + installation: 'jest-metadata-mock-url', + }, + monitoring: { + troubleshootKibana: 'jest-metadata-mock-url', + }, + }, }, }, }, diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap index 2f29cd9122a610..1173f36d620d62 100644 --- a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap @@ -156,7 +156,7 @@ exports[`Flyout apm part two should show instructions to migrate to metricbeat 1 "children":

({ shims: { kfetch: jest.fn(), docLinks: { - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', + links: { + monitoring: { + monitorKibana: 'jest-metadata-mock-url', + monitorElasticsearch: 'jest-metadata-mock-url', + }, + metricbeat: { + install: 'jest-metadata-mock-url', + configure: 'jest-metadata-mock-url', + }, + }, }, }, }, diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js index 1006468d0c7360..a0b5468cb9c771 100644 --- a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js @@ -14,10 +14,10 @@ import { Legacy } from '../../../../legacy_shims'; import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; - const securitySetup = getSecurityStep( - `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/configuring-howto-metricbeat.html` - ); + const metricbeatConfigUrl = Legacy.shims.docLinks.links.metricbeat.configure; + const metricbeatInstallUrl = Legacy.shims.docLinks.links.metricbeat.install; + const metricbeatStartUrl = Legacy.shims.docLinks.links.metricbeat.start; + const securitySetup = getSecurityStep(metricbeatConfigUrl); const installMetricbeatStep = { title: i18n.translate( @@ -29,10 +29,7 @@ export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMoni children: (

- +

- +

- +

- +

- +

- +

- +

- +

- +

- + }); const showIfLegacyOnlyIndices = () => { - const { ELASTIC_WEBSITE_URL } = Legacy.shims.docLinks; + const blogUrl = Legacy.shims.docLinks.links.monitoring.metricbeatBlog; const toast = Legacy.shims.toastNotifications.addWarning({ title: toMountPoint( { - + {learnMoreLabel()}

@@ -69,7 +65,7 @@ const showIfLegacyOnlyIndices = () => { }; const showIfLegacyAndMetricbeatIndices = () => { - const { ELASTIC_WEBSITE_URL } = Legacy.shims.docLinks; + const blogUrl = Legacy.shims.docLinks.links.monitoring.metricbeatBlog; const toast = Legacy.shims.toastNotifications.addWarning({ title: toMountPoint( { - + {learnMoreLabel()} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js index 7c01eea57e7230..325215d08af5f0 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js @@ -528,7 +528,7 @@ export class RemoteClusterForm extends Component { title={ } > diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap index 226002545a378f..76d284a21984e9 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap @@ -1,21 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action3\\" fails because [\\"action3\\" must be a boolean]]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action3]: expected value of type [boolean] but got [string]"`; -exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action2\\" fails because [\\"action2\\" is required]]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action2]: expected value of type [boolean] but got [undefined]"`; -exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"bar-resource\\" fails because [\\"bar-resource\\" is required]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"action4\\" is not allowed]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action4]: definition for this key is missing"`; -exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"otherApplication\\" is not allowed]"`; +exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.otherApplication]: definition for this key is missing"`; -exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"bar-resource\\" fails because [\\"bar-resource\\" is required]]]"`; +exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"application\\" is required]"`; +exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [\\"foo-application\\" is required]]"`; +exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when the resource propertry is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" must be an object]]]"`; +exports[`validateEsPrivilegeResponse fails validation when the resource propertry is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: could not parse object value from json input"`; -exports[`validateEsPrivilegeResponse fails validation when there are no resource properties in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`; +exports[`validateEsPrivilegeResponse fails validation when there are no resource properties in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index cfa6153c1b164a..93f5efed58fb8d 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -316,7 +316,7 @@ describe('#atSpace', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:bar-type/get]: definition for this key is missing]` ); }); @@ -338,7 +338,7 @@ describe('#atSpace', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:foo-type/get]: expected value of type [boolean] but got [undefined]]` ); }); }); @@ -1092,7 +1092,7 @@ describe('#atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [mock-action:version]: expected value of type [boolean] but got [undefined]]` ); }); @@ -1379,7 +1379,7 @@ describe('#atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected resources]` ); }); @@ -1407,7 +1407,7 @@ describe('#atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected resources]` ); }); @@ -1440,7 +1440,7 @@ describe('#atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected resources]` ); }); @@ -1463,7 +1463,7 @@ describe('#atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected resources]` ); }); }); @@ -2266,7 +2266,7 @@ describe('#globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [mock-action:version]: expected value of type [boolean] but got [undefined]]` ); }); @@ -2384,7 +2384,7 @@ describe('#globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:bar-type/get]: definition for this key is missing]` ); }); @@ -2405,7 +2405,7 @@ describe('#globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:foo-type/get]: expected value of type [boolean] but got [undefined]]` ); }); }); diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts index dbc5bdee8f250b..19afaaf035c15e 100644 --- a/x-pack/plugins/security/server/authorization/validate_es_response.ts +++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts @@ -5,7 +5,7 @@ * 2.0. */ -import Joi from 'joi'; +import { schema } from '@kbn/config-schema'; import { HasPrivilegesResponse } from './types'; export function validateEsPrivilegeResponse( @@ -14,48 +14,57 @@ export function validateEsPrivilegeResponse( actions: string[], resources: string[] ) { - const schema = buildValidationSchema(application, actions, resources); - const { error, value } = schema.validate(response); - - if (error) { - throw new Error( - `Invalid response received from Elasticsearch has_privilege endpoint. ${error}` - ); + const validationSchema = buildValidationSchema(application, actions, resources); + try { + validationSchema.validate(response); + } catch (e) { + throw new Error(`Invalid response received from Elasticsearch has_privilege endpoint. ${e}`); } - return value; + return response; } function buildActionsValidationSchema(actions: string[]) { - return Joi.object({ + return schema.object({ ...actions.reduce>((acc, action) => { return { ...acc, - [action]: Joi.bool().required(), + [action]: schema.boolean(), }; }, {}), - }).required(); + }); } function buildValidationSchema(application: string, actions: string[], resources: string[]) { const actionValidationSchema = buildActionsValidationSchema(actions); - const resourceValidationSchema = Joi.object({ - ...resources.reduce((acc, resource) => { - return { - ...acc, - [resource]: actionValidationSchema, - }; - }, {}), - }).required(); + const resourceValidationSchema = schema.object( + {}, + { + unknowns: 'allow', + validate: (value) => { + const actualResources = Object.keys(value).sort(); + if ( + resources.length !== actualResources.length || + !resources.sort().every((x, i) => x === actualResources[i]) + ) { + throw new Error('Payload did not match expected resources'); + } + + Object.values(value).forEach((actionResult) => { + actionValidationSchema.validate(actionResult); + }); + }, + } + ); - return Joi.object({ - username: Joi.string().required(), - has_all_requested: Joi.bool(), - cluster: Joi.object(), - application: Joi.object({ + return schema.object({ + username: schema.string(), + has_all_requested: schema.boolean(), + cluster: schema.object({}, { unknowns: 'allow' }), + application: schema.object({ [application]: resourceValidationSchema, - }).required(), - index: Joi.object(), - }).required(); + }), + index: schema.object({}, { unknowns: 'allow' }), + }); } diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index d4bae9d88d2624..ba64814cd1daf1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1277,6 +1277,7 @@ export class EndpointDocGenerator { status: agentPolicyStatuses.Active, description: 'Some description', namespace: 'default', + is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 2, updated_at: '2020-07-22T16:36:49.196Z', diff --git a/x-pack/plugins/security_solution/public/common/lib/lib.ts b/x-pack/plugins/security_solution/public/common/lib/lib.ts index e953fb1a341a39..7919ef78fff0b9 100644 --- a/x-pack/plugins/security_solution/public/common/lib/lib.ts +++ b/x-pack/plugins/security_solution/public/common/lib/lib.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { IScope } from 'angular'; import { NormalizedCacheObject } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; @@ -38,10 +37,3 @@ export interface AppKibanaUIConfig { // eslint-disable-next-line @typescript-eslint/no-explicit-any set(key: string, value: any): Promise; } - -export interface AppKibanaAdapterServiceRefs { - config: AppKibanaUIConfig; - rootScope: IScope; -} - -export type AppBufferedKibanaServiceCall = (serviceRefs: ServiceRefs) => void; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 5a99728f83b577..40900fdccdb28f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -38,7 +38,7 @@ export const updateRules = async ({ const enabled = ruleUpdate.enabled ?? true; const newInternalRule: InternalRuleUpdate = { name: ruleUpdate.name, - tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, false), + tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, existingRule.params.immutable), params: { author: ruleUpdate.author ?? [], buildingBlockType: ruleUpdate.building_block_type, diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts index 630e9c9c884895..5d1b090e98a798 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts @@ -19,6 +19,7 @@ export const mlServicesMock = { (({ modulesProvider: jest.fn(), jobServiceProvider: jest.fn(), + anomalyDetectorsProvider: jest.fn(), mlSystemProvider: createMockMlSystemProvider(), mlClient: createMockClient(), } as unknown) as jest.Mocked), diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 40867e566a7306..f5deb258fc1f48 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { cloneDeep, merge, unionBy } from 'lodash/fp'; +import { cloneDeep, merge } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -17,7 +17,7 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { getDataFromFieldsHits, getDataFromSourceHits } from './helpers'; +import { getDataFromSourceHits } from './helpers'; export const timelineEventsDetails: SecuritySolutionTimelineFactory = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => { @@ -29,7 +29,7 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const { _source, fields, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); + const { _source, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; @@ -42,13 +42,11 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory { + it('returns the expected query', () => { + const indexName = '.siem-signals-default'; + const eventId = 'f0a936d50b5b3a5a193d415459c14587fe633f7e519df7b5dc151d56142680e3'; + const docValueFields = [ + { field: '@timestamp' }, + { field: 'agent.ephemeral_id' }, + { field: 'agent.id' }, + { field: 'agent.name' }, + ]; + + const query = buildTimelineDetailsQuery(indexName, eventId, docValueFields); + + expect(query).toMatchInlineSnapshot(` + Object { + "allowNoIndices": true, + "body": Object { + "docvalue_fields": Array [ + Object { + "field": "@timestamp", + }, + Object { + "field": "agent.ephemeral_id", + }, + Object { + "field": "agent.id", + }, + Object { + "field": "agent.name", + }, + ], + "query": Object { + "terms": Object { + "_id": Array [ + "f0a936d50b5b3a5a193d415459c14587fe633f7e519df7b5dc151d56142680e3", + ], + }, + }, + }, + "ignoreUnavailable": true, + "index": ".siem-signals-default", + "size": 1, + } + `); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index a1265750271fad..e8890072c1aff6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -22,8 +22,6 @@ export const buildTimelineDetailsQuery = ( _id: [id], }, }, - fields: ['*'], - _source: ['signal.*'], }, size: 1, }); diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9126029139ef4d..981101bf733c75 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -8,13 +8,19 @@ import { CoreSetup, SavedObjectsClientContract } from '../../../../../src/core/server'; import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server'; import { CollectorDependencies } from './types'; -import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; +import { + DetectionsUsage, + fetchDetectionsUsage, + defaultDetectionsUsage, + fetchDetectionsMetrics, +} from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; export type RegisterCollector = (deps: CollectorDependencies) => void; export interface UsageData { detections: DetectionsUsage; endpoints: EndpointUsage | {}; + detectionMetrics: {}; } export async function getInternalSavedObjectsClient(core: CoreSetup) { @@ -57,6 +63,53 @@ export const registerCollector: RegisterCollector = ({ }, }, }, + detectionMetrics: { + ml_jobs: { + type: 'array', + items: { + job_id: { type: 'keyword' }, + open_time: { type: 'keyword' }, + create_time: { type: 'keyword' }, + finished_time: { type: 'keyword' }, + state: { type: 'keyword' }, + data_counts: { + bucket_count: { type: 'long' }, + empty_bucket_count: { type: 'long' }, + input_bytes: { type: 'long' }, + input_record_count: { type: 'long' }, + last_data_time: { type: 'long' }, + processed_record_count: { type: 'long' }, + }, + model_size_stats: { + bucket_allocation_failures_count: { type: 'long' }, + model_bytes: { type: 'long' }, + model_bytes_exceeded: { type: 'long' }, + model_bytes_memory_limit: { type: 'long' }, + peak_model_bytes: { type: 'long' }, + }, + timing_stats: { + average_bucket_processing_time_ms: { type: 'long' }, + bucket_count: { type: 'long' }, + exponential_average_bucket_processing_time_ms: { type: 'long' }, + exponential_average_bucket_processing_time_per_hour_ms: { type: 'long' }, + maximum_bucket_processing_time_ms: { type: 'long' }, + minimum_bucket_processing_time_ms: { type: 'long' }, + total_bucket_processing_time_ms: { type: 'long' }, + }, + datafeed: { + datafeed_id: { type: 'keyword' }, + state: { type: 'keyword' }, + timing_stats: { + average_search_time_per_bucket_ms: { type: 'long' }, + bucket_count: { type: 'long' }, + exponential_average_search_time_per_hour_ms: { type: 'long' }, + search_count: { type: 'long' }, + total_search_time_ms: { type: 'long' }, + }, + }, + }, + }, + }, endpoints: { total_installed: { type: 'long' }, active_within_last_24_hours: { type: 'long' }, @@ -80,19 +133,17 @@ export const registerCollector: RegisterCollector = ({ }, isReady: () => kibanaIndex.length > 0, fetch: async ({ esClient }: CollectorFetchContext): Promise => { - const savedObjectsClient = await getInternalSavedObjectsClient(core); - const [detections, endpoints] = await Promise.allSettled([ - fetchDetectionsUsage( - kibanaIndex, - esClient, - ml, - (savedObjectsClient as unknown) as SavedObjectsClientContract - ), - getEndpointTelemetryFromFleet(savedObjectsClient), + const internalSavedObjectsClient = await getInternalSavedObjectsClient(core); + const savedObjectsClient = (internalSavedObjectsClient as unknown) as SavedObjectsClientContract; + const [detections, detectionMetrics, endpoints] = await Promise.allSettled([ + fetchDetectionsUsage(kibanaIndex, esClient, ml, savedObjectsClient), + fetchDetectionsMetrics(ml, savedObjectsClient), + getEndpointTelemetryFromFleet(internalSavedObjectsClient), ]); return { detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage, + detectionMetrics: detectionMetrics.status === 'fulfilled' ? detectionMetrics.value : {}, endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {}, }; }, diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts index 5601250ac1ecdb..f7fa59958abae4 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts @@ -175,3 +175,130 @@ export const getMockRulesResponse = () => ({ ], }, }); + +export const getMockMlJobDetailsResponse = () => ({ + count: 20, + jobs: [ + { + job_id: 'high_distinct_count_error_message', + job_type: 'anomaly_detector', + job_version: '8.0.0', + create_time: 1603838214983, + finished_time: 1611739871669, + model_snapshot_id: '1611740107', + custom_settings: { + created_by: undefined, + }, + groups: ['cloudtrail', 'security'], + description: + 'Security: Cloudtrail - Looks for a spike in the rate of an error message which may simply indicate an impending service failure but these can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'high_distinct_count("aws.cloudtrail.error_message")', + function: 'high_distinct_count', + field_name: 'aws.cloudtrail.error_message', + detector_index: 0, + }, + ], + influencers: ['aws.cloudtrail.user_identity.arn', 'source.ip', 'source.geo.city_name'], + }, + analysis_limits: { + model_memory_limit: '16mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'custom-high_distinct_count_error_message', + }, + ], +}); + +export const getMockMlJobStatsResponse = () => ({ + count: 1, + jobs: [ + { + job_id: 'high_distinct_count_error_message', + data_counts: { + job_id: 'high_distinct_count_error_message', + processed_record_count: 162, + processed_field_count: 476, + input_bytes: 45957, + input_field_count: 476, + invalid_date_count: 0, + missing_field_count: 172, + out_of_order_timestamp_count: 0, + empty_bucket_count: 8590, + sparse_bucket_count: 0, + bucket_count: 8612, + earliest_record_timestamp: 1602648289000, + latest_record_timestamp: 1610399348000, + last_data_time: 1610470367123, + latest_empty_bucket_timestamp: 1610397900000, + input_record_count: 162, + log_time: 1610470367123, + }, + model_size_stats: { + job_id: 'high_distinct_count_error_message', + result_type: 'model_size_stats', + model_bytes: 72574, + peak_model_bytes: 78682, + model_bytes_exceeded: 0, + model_bytes_memory_limit: 16777216, + total_by_field_count: 4, + total_over_field_count: 0, + total_partition_field_count: 3, + bucket_allocation_failures_count: 0, + memory_status: 'ok', + assignment_memory_basis: 'current_model_bytes', + categorized_doc_count: 0, + total_category_count: 0, + frequent_category_count: 0, + rare_category_count: 0, + dead_category_count: 0, + failed_category_count: 0, + categorization_status: 'ok', + log_time: 1611740107843, + timestamp: 1611738900000, + }, + forecasts_stats: { + total: 0, + forecasted_jobs: 0, + }, + state: 'closed', + timing_stats: { + job_id: 'high_distinct_count_error_message', + bucket_count: 16236, + total_bucket_processing_time_ms: 7957.00000000008, + minimum_bucket_processing_time_ms: 0, + maximum_bucket_processing_time_ms: 392, + average_bucket_processing_time_ms: 0.4900837644740133, + exponential_average_bucket_processing_time_ms: 0.23614068552903306, + exponential_average_bucket_processing_time_per_hour_ms: 1.5551298175461634, + }, + }, + ], +}); + +export const getMockMlDatafeedStatsResponse = () => ({ + count: 1, + datafeeds: [ + { + datafeed_id: 'datafeed-high_distinct_count_error_message', + state: 'stopped', + timing_stats: { + job_id: 'high_distinct_count_error_message', + search_count: 7202, + bucket_count: 8612, + total_search_time_ms: 3107147, + average_search_time_per_bucket_ms: 360.7927310729215, + exponential_average_search_time_per_hour_ms: 86145.39799630083, + }, + }, + ], +}); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index 1804d7c756e53a..b53f90f40f6216 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -12,15 +12,18 @@ import { getMockJobSummaryResponse, getMockListModulesResponse, getMockRulesResponse, + getMockMlJobDetailsResponse, + getMockMlJobStatsResponse, + getMockMlDatafeedStatsResponse, } from './detections.mocks'; -import { fetchDetectionsUsage } from './index'; +import { fetchDetectionsUsage, fetchDetectionsMetrics } from './index'; -describe('Detections Usage', () => { - describe('fetchDetectionsUsage()', () => { - let esClientMock: jest.Mocked; - let savedObjectsClientMock: jest.Mocked; - let mlMock: ReturnType; +describe('Detections Usage and Metrics', () => { + let esClientMock: jest.Mocked; + let savedObjectsClientMock: jest.Mocked; + let mlMock: ReturnType; + describe('fetchDetectionsUsage()', () => { beforeEach(() => { esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; mlMock = mlServicesMock.create(); @@ -102,4 +105,89 @@ describe('Detections Usage', () => { ); }); }); + + describe('fetchDetectionsMetrics()', () => { + beforeEach(() => { + mlMock = mlServicesMock.create(); + }); + + it('returns an empty array if there is no data', async () => { + mlMock.anomalyDetectorsProvider.mockReturnValue(({ + jobs: null, + jobStats: null, + } as unknown) as ReturnType); + const result = await fetchDetectionsMetrics(mlMock, savedObjectsClientMock); + + expect(result).toEqual( + expect.objectContaining({ + ml_jobs: [], + }) + ); + }); + + it('returns an ml job telemetry object from anomaly detectors provider', async () => { + const mockJobsResponse = jest.fn().mockResolvedValue(getMockMlJobDetailsResponse()); + const mockJobStatsResponse = jest.fn().mockResolvedValue(getMockMlJobStatsResponse()); + const mockDatafeedStatsResponse = jest + .fn() + .mockResolvedValue(getMockMlDatafeedStatsResponse()); + + mlMock.anomalyDetectorsProvider.mockReturnValue(({ + jobs: mockJobsResponse, + jobStats: mockJobStatsResponse, + datafeedStats: mockDatafeedStatsResponse, + } as unknown) as ReturnType); + + const result = await fetchDetectionsMetrics(mlMock, savedObjectsClientMock); + + expect(result).toEqual( + expect.objectContaining({ + ml_jobs: [ + { + job_id: 'high_distinct_count_error_message', + create_time: 1603838214983, + finished_time: 1611739871669, + state: 'closed', + data_counts: { + bucket_count: 8612, + empty_bucket_count: 8590, + input_bytes: 45957, + input_record_count: 162, + last_data_time: 1610470367123, + processed_record_count: 162, + }, + model_size_stats: { + bucket_allocation_failures_count: 0, + memory_status: 'ok', + model_bytes: 72574, + model_bytes_exceeded: 0, + model_bytes_memory_limit: 16777216, + peak_model_bytes: 78682, + }, + timing_stats: { + average_bucket_processing_time_ms: 0.4900837644740133, + bucket_count: 16236, + exponential_average_bucket_processing_time_ms: 0.23614068552903306, + exponential_average_bucket_processing_time_per_hour_ms: 1.5551298175461634, + maximum_bucket_processing_time_ms: 392, + minimum_bucket_processing_time_ms: 0, + total_bucket_processing_time_ms: 7957.00000000008, + }, + datafeed: { + datafeed_id: 'datafeed-high_distinct_count_error_message', + state: 'stopped', + timing_stats: { + average_search_time_per_bucket_ms: 360.7927310729215, + bucket_count: 8612, + exponential_average_search_time_per_hour_ms: 86145.39799630083, + search_count: 7202, + total_search_time_ms: 3107147, + }, + }, + }, + ], + }) + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index 9ffd3e09117796..4236c782d6c685 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -13,7 +13,7 @@ import { } from '../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../ml/server'; import { SIGNALS_ID, INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; -import { DetectionRulesUsage, MlJobsUsage } from './index'; +import { DetectionRulesUsage, MlJobsUsage, MlJobMetric } from './index'; import { isJobStarted } from '../../../common/machine_learning/helpers'; import { isSecurityJob } from '../../../common/machine_learning/is_security_job'; @@ -213,3 +213,93 @@ export const getMlJobsUsage = async ( return jobsUsage; }; + +export const getMlJobMetrics = async ( + ml: MlPluginSetup | undefined, + savedObjectClient: SavedObjectsClientContract +): Promise => { + if (ml) { + try { + const fakeRequest = { headers: {} } as KibanaRequest; + const jobsType = 'security'; + const securityJobStats = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .jobStats(jobsType); + + const jobDetails = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .jobs(jobsType); + + const jobDetailsCache = new Map(); + jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail)); + + const datafeedStats = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .datafeedStats(); + + const datafeedStatsCache = new Map(); + datafeedStats.datafeeds.forEach((datafeedStat) => + datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat) + ); + + return securityJobStats.jobs.map((stat) => { + const jobId = stat.job_id; + const jobDetail = jobDetailsCache.get(stat.job_id); + const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`); + + return { + job_id: jobId, + open_time: stat.open_time, + create_time: jobDetail?.create_time, + finished_time: jobDetail?.finished_time, + state: stat.state, + data_counts: { + bucket_count: stat.data_counts.bucket_count, + empty_bucket_count: stat.data_counts.empty_bucket_count, + input_bytes: stat.data_counts.input_bytes, + input_record_count: stat.data_counts.input_record_count, + last_data_time: stat.data_counts.last_data_time, + processed_record_count: stat.data_counts.processed_record_count, + }, + model_size_stats: { + bucket_allocation_failures_count: + stat.model_size_stats.bucket_allocation_failures_count, + memory_status: stat.model_size_stats.memory_status, + model_bytes: stat.model_size_stats.model_bytes, + model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded, + model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit, + peak_model_bytes: stat.model_size_stats.peak_model_bytes, + }, + timing_stats: { + average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms, + bucket_count: stat.timing_stats.bucket_count, + exponential_average_bucket_processing_time_ms: + stat.timing_stats.exponential_average_bucket_processing_time_ms, + exponential_average_bucket_processing_time_per_hour_ms: + stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms, + maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms, + minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms, + total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms, + }, + datafeed: { + datafeed_id: datafeed?.datafeed_id, + state: datafeed?.state, + timing_stats: { + average_search_time_per_bucket_ms: + datafeed?.timing_stats.average_search_time_per_bucket_ms, + bucket_count: datafeed?.timing_stats.bucket_count, + exponential_average_search_time_per_hour_ms: + datafeed?.timing_stats.exponential_average_search_time_per_hour_ms, + search_count: datafeed?.timing_stats.search_count, + total_search_time_ms: datafeed?.timing_stats.total_search_time_ms, + }, + }, + } as MlJobMetric; + }); + } catch (e) { + // ignore failure, usage will be zeroed + } + } + + return []; +}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index 27f0b1acb2ee96..39c8f3159fe037 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; import { getMlJobsUsage, + getMlJobMetrics, getRulesUsage, initialRulesUsage, initialMlJobsUsage, @@ -34,6 +35,47 @@ export interface DetectionsUsage { ml_jobs: MlJobsUsage; } +export interface DetectionMetrics { + ml_jobs: MlJobMetric[]; +} + +export interface MlJobDataCount { + bucket_count: number; + empty_bucket_count: number; + input_bytes: number; + input_record_count: number; + last_data_time: number; + processed_record_count: number; +} + +export interface MlJobModelSize { + bucket_allocation_failures_count: number; + memory_status: string; + model_bytes: number; + model_bytes_exceeded: number; + model_bytes_memory_limit: number; + peak_model_bytes: number; +} + +export interface MlTimingStats { + average_bucket_processing_time_ms: number; + bucket_count: number; + exponential_average_bucket_processing_time_ms: number; + exponential_average_bucket_processing_time_per_hour_ms: number; + maximum_bucket_processing_time_ms: number; + minimum_bucket_processing_time_ms: number; + total_bucket_processing_time_ms: number; +} + +export interface MlJobMetric { + job_id: string; + open_time: string; + state: string; + data_counts: MlJobDataCount; + model_size_stats: MlJobModelSize; + timing_stats: MlTimingStats; +} + export const defaultDetectionsUsage = { detection_rules: initialRulesUsage, ml_jobs: initialMlJobsUsage, @@ -55,3 +97,14 @@ export const fetchDetectionsUsage = async ( ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage, }; }; + +export const fetchDetectionsMetrics = async ( + ml: MlPluginSetup | undefined, + savedObjectClient: SavedObjectsClientContract +): Promise => { + const [mlJobMetrics] = await Promise.allSettled([getMlJobMetrics(ml, savedObjectClient)]); + + return { + ml_jobs: mlJobMetrics.status === 'fulfilled' ? mlJobMetrics.value : [], + }; +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md new file mode 100644 index 00000000000000..798beed8d17bd9 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md @@ -0,0 +1,123 @@ +## Instructions for loading & observing data + +There are several steps required to set up geo containment alerts for testing in a way +that allows you to view triggered alerts as they happen. These instructions outline +how to load test data, but really these steps can be used to load any data for geo +containment alerts so long as you have the following data: +- An index containing a`geo_point` field and a `date` field. This data is presumed to +be dynamic (updated). +- An index containing `geo_shape` data, such as boundary data, bounding box data, etc. +This data is presumed to be static (not updated). Shape data matching the query is +harvested once when the alert is created and anytime after when alert is re-enabled +after disablement +The ability for containment alerts to monitor data requires there be somewhat "real time" +data streaming in as indicated by the `date` field. + +### 1. Set experimental flag to enable containment alerts +- Your `kibana.yml` config file is located in the `config/` dir in the base of your kibana +project. To edit it, open this file in your editor of choice, add the line described in +the next step to the bottom of the file (or really anywhere) and save. For more details +on different config modifications or on how to make production config modifications, +see [the current docs](https://www.elastic.co/guide/en/kibana/current/settings.html) +- Set the following configuration settings in your `config/kibana.yml`: +`xpack.stack_alerts.enableGeoAlerting: true` + +### 2. Run ES/Kibana dev env with ssl enabled +- In two terminals, run the normal commands to launch both elasticsearch and kibana but +append `--ssl` to the end of each as an arg, i.e.: + - `yarn es snapshot --ssl # Runs Elasticsearch` + - `yarn start --ssl # Runs Kibana` + +### 3. Get an MTA data api key +- You'll need to obtain an NYC MTA api key, you can request this + key [here](https://docs.google.com/forms/d/e/1FAIpQLSfGUZA6h4eHd2-ImaK5Q_I5Gb7C3UEP5vYDALyGd7r3h08YKg/viewform?hl=en&formkey=dG9kcGIxRFpSS0NhQWM4UjA0V0VkNGc6MQ#gid=0) + +### 4. Get trackable point data (MTA bus data) into elasticsearch +- You'll be using the script: `https://github.com/thomasneirynck/mtatracks` to harvest +live bus data to populate the system. Clone the repo and follow the instructions in +the readme to set up. +- Using the MTA key you obtained in the previous step, the final command to run +in a local terminal should look something like the following. This script loads large +quantities of data the frequency listed below (20000ms = 20s) or higher: +`node ./load_tracks.js -a -f 20000` + +### 5. Open required Kibana tabs +There are 3 separate tabs you'll need for a combination of loading and viewing the +data. Since you'll be jumping between them, it might be easiest to just open them +upfront. Each is preceded by `https://localhost:5601//app/`: +- Stack Management > Index Patterns: `management/kibana/indexPatterns` +- Stack Management > Alerts & Actions: `management/insightsAndAlerting/triggersActions/alerts` +- Maps: `maps` + +### 6 Create map to monitor alerts +- Go to the Maps app and create a new map +- Using GeoJSON Upload, upload the GeoJSON file located in the folder of the previously +cloned `mta_tracks` repo: `nyc-neighborhoods.geo.json`. Accept all of the default +settings and add the layer. +- You may want to click your newly added layer and select "Fit to data" so you can see the +boundaries you've added. +_ When finished uploading and adding the layer, save the map using a name of your +choice. +- Keep the Maps tab open, you'll come back to this + +### 7. Create index pattern for generated tracks +- Go to the index pattern tab to create a new index pattern. +- Give it the index name `mtatracks*` +- For `Time field` select `@timestamp` +- Click `Create index pattern` +- Leave this tab open, you'll come back to this + +### 8. Create containment alert +- Go to the Alerts tab and click `Create Alert` > `Tracking containment` +- Fill the side bar form top to bottom. This _should_ flow somewhat logically. In the top +section, set both `Check every` and `Notify every` to `1 minute`. + For `Notify`, leave +on default selected option `Only on status change`, this will notify only on newly +contained entities. + **Please note that `2 seconds` is an unusually quick interval but done here for demo + purposes. With real world data, setting an appropriate interval speed is highly dependent + upon the quantity, update frequency and complexity of data handled.** +- The default settings for `Select Entity` will mostly be correct. Select `mta_tracks*` +as the index you'd like to track. Use the defaults populated under +`Select entity` > `INDEX`, update `Select entity` > `BY` to `vehicle_ref`. +- For `Select boundary` > `INDEX`, select `nyc-neighborhoods` and all populated defaults. +- Under `Actions`, create an `Server log` action, then create a `Connector` which you can simply name +`Log test`. +- For `Run when`, the default `Tracking containment met` will work here. This will track +only points that are newly contained in the boundaries. +- Leave the log level at `Info` +- For the message, use the following sample message or one of your own: +``` +Entity: {{context.entityId}} with document ID: {{context.entityDocumentId}} has been recorded at location: {{context.entityLocation}} in boundary: {{context.containingBoundaryName}}({{context.containingBoundaryId}}) at {{context.entityDateTime}}. This was detected by the alerting framework at: {{context.detectionDateTime}}. +``` +- At the bottom right, click `Save`. Your alert should now be created! +- You should now be able to see alerts generated in your Kibana console log. + +### 9. Visually confirm your alerts with Maps +- Creating layers + - Using the source data below, you can create the following layers: + - Boundary data (`nyc-neighborhoods`) + - Boundary layer + - Original tracks data (`mtatracks*`) + - Last known location + - Geo-line track + - Boundary layer + - This layer should already be added from when you uploaded the GeoJSON + file earlier. If it's not already added, it can be added by selecting `Documents` + > `Index patterns` > `nyc-neighborhoods` then accept the defaults and add the layer. + - Vehicle tracks + - Add `Tracks` > `Index patterns` > `mtatracks*`, accept the defaults selected and set `Entity` > `entity_id`. Add the layer and style appropriately. + - Last known location + - Add `Documents` > `Index patterns` > `mtatracks*` and select `Show top hits per entity` + - For `Entity` select `entity_id` and add the layer. + - The only required setting on the following screen is to set `Sorting` to sort on `@timestamp` +- Update time scope of data + - Changing the refresh rate `Refresh every`: `4 seconds` keeps the layers updated and in particular + shows the latest values obtained in the `Top hits` layer + - The time picker should already be set to the default `15 minutes`, this is a good default but + can be adjusted up or down to see more or less data respectively +- General tips + - Style layers with contrasting colors to clearly see each + - Consider using icons for the `Top hits` vehicle movement layer + - Consider adding tooltips to layers to better understand the data in your layers. + - Save your Map anytime you've made any layer adjustments diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index c1674f8a926699..9e6a0c06808bc8 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3244,6 +3244,127 @@ } } }, + "detectionMetrics": { + "properties": { + "ml_jobs": { + "type": "array", + "items": { + "properties": { + "job_id": { + "type": "keyword" + }, + "open_time": { + "type": "keyword" + }, + "create_time": { + "type": "keyword" + }, + "finished_time": { + "type": "keyword" + }, + "state": { + "type": "keyword" + }, + "data_counts": { + "properties": { + "bucket_count": { + "type": "long" + }, + "empty_bucket_count": { + "type": "long" + }, + "input_bytes": { + "type": "long" + }, + "input_record_count": { + "type": "long" + }, + "last_data_time": { + "type": "long" + }, + "processed_record_count": { + "type": "long" + } + } + }, + "model_size_stats": { + "properties": { + "bucket_allocation_failures_count": { + "type": "long" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "long" + }, + "model_bytes_memory_limit": { + "type": "long" + }, + "peak_model_bytes": { + "type": "long" + } + } + }, + "timing_stats": { + "properties": { + "average_bucket_processing_time_ms": { + "type": "long" + }, + "bucket_count": { + "type": "long" + }, + "exponential_average_bucket_processing_time_ms": { + "type": "long" + }, + "exponential_average_bucket_processing_time_per_hour_ms": { + "type": "long" + }, + "maximum_bucket_processing_time_ms": { + "type": "long" + }, + "minimum_bucket_processing_time_ms": { + "type": "long" + }, + "total_bucket_processing_time_ms": { + "type": "long" + } + } + }, + "datafeed": { + "properties": { + "datafeed_id": { + "type": "keyword" + }, + "state": { + "type": "keyword" + }, + "timing_stats": { + "properties": { + "average_search_time_per_bucket_ms": { + "type": "long" + }, + "bucket_count": { + "type": "long" + }, + "exponential_average_search_time_per_hour_ms": { + "type": "long" + }, + "search_count": { + "type": "long" + }, + "total_search_time_ms": { + "type": "long" + } + } + } + } + } + } + } + } + } + }, "endpoints": { "properties": { "total_installed": { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cab6973072f24f..3cfaaba5f8538e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3367,14 +3367,14 @@ "tileMap.geohashLayer.mapTitle": "{mapType} マップタイプが認識されません", "tileMap.tooltipFormatter.latitudeLabel": "緯度", "tileMap.tooltipFormatter.longitudeLabel": "経度", - "tileMap.vis.editorConfig.legendPositions.bottomLeftText": "左下", - "tileMap.vis.editorConfig.legendPositions.bottomRightText": "右下", - "tileMap.vis.editorConfig.legendPositions.topLeftText": "左上", - "tileMap.vis.editorConfig.legendPositions.topRightText": "右上", - "tileMap.vis.editorConfig.mapTypes.heatmapText": "ヒートマップ", - "tileMap.vis.editorConfig.mapTypes.scaledCircleMarkersText": "スケーリングされた円マーカー", - "tileMap.vis.editorConfig.mapTypes.shadedCircleMarkersText": "影付き円マーカー", - "tileMap.vis.editorConfig.mapTypes.shadedGeohashGridText": "影付きジオハッシュグリッド", + "tileMap.legendPositions.bottomLeftText": "左下", + "tileMap.legendPositions.bottomRightText": "右下", + "tileMap.legendPositions.topLeftText": "左上", + "tileMap.legendPositions.topRightText": "右上", + "tileMap.mapTypes.heatmapText": "ヒートマップ", + "tileMap.mapTypes.scaledCircleMarkersText": "スケーリングされた円マーカー", + "tileMap.mapTypes.shadedCircleMarkersText": "影付き円マーカー", + "tileMap.mapTypes.shadedGeohashGridText": "影付きジオハッシュグリッド", "tileMap.vis.map.editorConfig.schemas.geoCoordinatesTitle": "座標", "tileMap.vis.map.editorConfig.schemas.metricTitle": "値", "tileMap.vis.mapDescription": "マップ上に緯度と経度の座標を表示します。", @@ -3967,12 +3967,12 @@ "visTypeTagCloud.function.metric.help": "メトリックディメンションの構成です。", "visTypeTagCloud.function.orientation.help": "タグクラウド内の単語の方向です。", "visTypeTagCloud.function.scale.help": "単語のフォントサイズを決定するスケールです", - "visTypeTagCloud.vis.editorConfig.orientations.multipleText": "複数", - "visTypeTagCloud.vis.editorConfig.orientations.rightAngledText": "直角", - "visTypeTagCloud.vis.editorConfig.orientations.singleText": "単一", - "visTypeTagCloud.vis.editorConfig.scales.linearText": "線形", - "visTypeTagCloud.vis.editorConfig.scales.logText": "ログ", - "visTypeTagCloud.vis.editorConfig.scales.squareRootText": "平方根", + "visTypeTagCloud.orientations.multipleText": "複数", + "visTypeTagCloud.orientations.rightAngledText": "直角", + "visTypeTagCloud.orientations.singleText": "単一", + "visTypeTagCloud.scales.linearText": "線形", + "visTypeTagCloud.scales.logText": "ログ", + "visTypeTagCloud.scales.squareRootText": "平方根", "visTypeTagCloud.vis.schemas.metricTitle": "タグサイズ", "visTypeTagCloud.vis.schemas.segmentTitle": "タグ", "visTypeTagCloud.vis.tagCloudDescription": "単語の頻度とフォントサイズを表示します。", @@ -5104,7 +5104,6 @@ "xpack.apm.localFilters.titles.serviceName": "サービス名", "xpack.apm.localFilters.titles.serviceVersion": "サービスバージョン", "xpack.apm.localFilters.titles.transactionResult": "トランザクション結果", - "xpack.apm.localFilters.titles.transactionType": "トランザクションタイプ", "xpack.apm.localFilters.titles.transactionUrl": "Url", "xpack.apm.localFiltersTitle": "フィルター", "xpack.apm.metadataTable.section.agentLabel": "エージェント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2bdbfc3d565e5b..ebc0c6f88ecfa9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3371,14 +3371,14 @@ "tileMap.geohashLayer.mapTitle": "{mapType} 地图类型无法识别", "tileMap.tooltipFormatter.latitudeLabel": "纬度", "tileMap.tooltipFormatter.longitudeLabel": "经度", - "tileMap.vis.editorConfig.legendPositions.bottomLeftText": "左下方", - "tileMap.vis.editorConfig.legendPositions.bottomRightText": "右下方", - "tileMap.vis.editorConfig.legendPositions.topLeftText": "左上方", - "tileMap.vis.editorConfig.legendPositions.topRightText": "右上方", - "tileMap.vis.editorConfig.mapTypes.heatmapText": "热图", - "tileMap.vis.editorConfig.mapTypes.scaledCircleMarkersText": "缩放式圆形标记", - "tileMap.vis.editorConfig.mapTypes.shadedCircleMarkersText": "带阴影圆形标记", - "tileMap.vis.editorConfig.mapTypes.shadedGeohashGridText": "带阴影 geohash 网格", + "tileMap.legendPositions.bottomLeftText": "左下方", + "tileMap.legendPositions.bottomRightText": "右下方", + "tileMap.legendPositions.topLeftText": "左上方", + "tileMap.legendPositions.topRightText": "右上方", + "tileMap.mapTypes.heatmapText": "热图", + "tileMap.mapTypes.scaledCircleMarkersText": "缩放式圆形标记", + "tileMap.mapTypes.shadedCircleMarkersText": "带阴影圆形标记", + "tileMap.mapTypes.shadedGeohashGridText": "带阴影 geohash 网格", "tileMap.vis.map.editorConfig.schemas.geoCoordinatesTitle": "地理坐标", "tileMap.vis.map.editorConfig.schemas.metricTitle": "值", "tileMap.vis.mapDescription": "在地图上绘制纬度和经度坐标", @@ -3971,12 +3971,12 @@ "visTypeTagCloud.function.metric.help": "指标维度配置", "visTypeTagCloud.function.orientation.help": "标签云图内的字方向", "visTypeTagCloud.function.scale.help": "缩放以确定字体大小", - "visTypeTagCloud.vis.editorConfig.orientations.multipleText": "多个", - "visTypeTagCloud.vis.editorConfig.orientations.rightAngledText": "直角", - "visTypeTagCloud.vis.editorConfig.orientations.singleText": "单个", - "visTypeTagCloud.vis.editorConfig.scales.linearText": "线性", - "visTypeTagCloud.vis.editorConfig.scales.logText": "对数", - "visTypeTagCloud.vis.editorConfig.scales.squareRootText": "平方根", + "visTypeTagCloud.orientations.multipleText": "多个", + "visTypeTagCloud.orientations.rightAngledText": "直角", + "visTypeTagCloud.orientations.singleText": "单个", + "visTypeTagCloud.scales.linearText": "线性", + "visTypeTagCloud.scales.logText": "对数", + "visTypeTagCloud.scales.squareRootText": "平方根", "visTypeTagCloud.vis.schemas.metricTitle": "标签大小", "visTypeTagCloud.vis.schemas.segmentTitle": "标签", "visTypeTagCloud.vis.tagCloudDescription": "使用字体大小显示词频。", @@ -5112,7 +5112,6 @@ "xpack.apm.localFilters.titles.serviceName": "服务名称", "xpack.apm.localFilters.titles.serviceVersion": "服务版本", "xpack.apm.localFilters.titles.transactionResult": "事务结果", - "xpack.apm.localFilters.titles.transactionType": "事务类型", "xpack.apm.localFilters.titles.transactionUrl": "URL", "xpack.apm.localFiltersTitle": "筛选", "xpack.apm.metadataTable.section.agentLabel": "代理", diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index c204ec3b28cf0d..2705406009062d 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -16,464 +16,373 @@ const INDEX_NAME = 'filebeat-7.0.0-iot-2019.06'; const ID = 'QRhG1WgBqd-n62SwZYDT'; const EXPECTED_DATA = [ { - category: 'file', - field: 'file.path', - values: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], - originalValue: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], - }, - { - category: 'traefik', - field: 'traefik.access.geoip.region_iso_code', - values: ['US-WA'], - originalValue: ['US-WA'], - }, - { - category: 'host', - field: 'host.hostname', - values: ['raspberrypi'], - originalValue: ['raspberrypi'], - }, - { - category: 'traefik', - field: 'traefik.access.geoip.location', - values: ['{"long":-122.3341,"lat":47.6103}'], - originalValue: ['{"coordinates":[-122.3341,47.6103],"type":"Point"}'], + category: 'base', + field: '@timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: '2019-02-10T02:39:44.107Z', }, { - category: 'suricata', - field: 'suricata.eve.src_port', - values: ['80'], - originalValue: ['80'], + category: '@version', + field: '@version', + values: ['1'], + originalValue: '1', }, { - category: 'traefik', - field: 'traefik.access.geoip.city_name', - values: ['Seattle'], - originalValue: ['Seattle'], + category: 'agent', + field: 'agent.ephemeral_id', + values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', }, { - category: 'service', - field: 'service.type', - values: ['suricata'], - originalValue: ['suricata'], + category: 'agent', + field: 'agent.hostname', + values: ['raspberrypi'], + originalValue: 'raspberrypi', }, { - category: 'http', - field: 'http.request.method', - values: ['get'], - originalValue: ['get'], + category: 'agent', + field: 'agent.id', + values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', }, { - category: 'host', - field: 'host.os.version', - values: ['9 (stretch)'], - originalValue: ['9 (stretch)'], + category: 'agent', + field: 'agent.type', + values: ['filebeat'], + originalValue: 'filebeat', }, { - category: 'source', - field: 'source.geo.region_name', - values: ['Washington'], - originalValue: ['Washington'], + category: 'agent', + field: 'agent.version', + values: ['7.0.0'], + originalValue: '7.0.0', }, { - category: 'suricata', - field: 'suricata.eve.http.protocol', - values: ['HTTP/1.1'], - originalValue: ['HTTP/1.1'], + category: 'destination', + field: 'destination.domain', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', }, { - category: 'host', - field: 'host.os.name', - values: ['Raspbian GNU/Linux'], - originalValue: ['Raspbian GNU/Linux'], + category: 'destination', + field: 'destination.ip', + values: ['10.100.7.196'], + originalValue: '10.100.7.196', }, { - category: 'source', - field: 'source.ip', - values: ['54.239.219.210'], - originalValue: ['54.239.219.210'], + category: 'destination', + field: 'destination.port', + values: [40684], + originalValue: 40684, }, { - category: 'host', - field: 'host.name', - values: ['raspberrypi'], - originalValue: ['raspberrypi'], + category: 'ecs', + field: 'ecs.version', + values: ['1.0.0-beta2'], + originalValue: '1.0.0-beta2', }, { - category: 'source', - field: 'source.geo.region_iso_code', - values: ['US-WA'], - originalValue: ['US-WA'], + category: 'event', + field: 'event.dataset', + values: ['suricata.eve'], + originalValue: 'suricata.eve', }, { - category: 'http', - field: 'http.response.status_code', - values: ['206'], - originalValue: ['206'], + category: 'event', + field: 'event.end', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: '2019-02-10T02:39:44.107Z', }, { category: 'event', field: 'event.kind', values: ['event'], - originalValue: ['event'], + originalValue: 'event', }, { - category: 'suricata', - field: 'suricata.eve.flow_id', - values: ['196625917175466'], - originalValue: ['196625917175466'], - }, - { - category: 'source', - field: 'source.geo.city_name', - values: ['Seattle'], - originalValue: ['Seattle'], + category: 'event', + field: 'event.module', + values: ['suricata'], + originalValue: 'suricata', }, { - category: 'suricata', - field: 'suricata.eve.proto', - values: ['tcp'], - originalValue: ['tcp'], + category: 'event', + field: 'event.type', + values: ['fileinfo'], + originalValue: 'fileinfo', }, { - category: 'flow', - field: 'flow.locality', - values: ['public'], - originalValue: ['public'], + category: 'file', + field: 'file.path', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', }, { - category: 'traefik', - field: 'traefik.access.geoip.country_iso_code', - values: ['US'], - originalValue: ['US'], + category: 'file', + field: 'file.size', + values: [48277], + originalValue: 48277, }, { category: 'fileset', field: 'fileset.name', values: ['eve'], - originalValue: ['eve'], + originalValue: 'eve', }, { - category: 'input', - field: 'input.type', - values: ['log'], - originalValue: ['log'], - }, - { - category: 'log', - field: 'log.offset', - values: ['1856288115'], - originalValue: ['1856288115'], + category: 'flow', + field: 'flow.locality', + values: ['public'], + originalValue: 'public', }, { - category: 'destination', - field: 'destination.domain', - values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + category: 'host', + field: 'host.architecture', + values: ['armv7l'], + originalValue: 'armv7l', }, { - category: 'agent', - field: 'agent.hostname', + category: 'host', + field: 'host.hostname', values: ['raspberrypi'], - originalValue: ['raspberrypi'], - }, - { - category: 'suricata', - field: 'suricata.eve.http.hostname', - values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: 'raspberrypi', }, { - category: 'suricata', - field: 'suricata.eve.in_iface', - values: ['eth0'], - originalValue: ['eth0'], - }, - { - category: 'base', - field: 'tags', - values: ['suricata'], - originalValue: ['suricata'], + category: 'host', + field: 'host.id', + values: ['b19a781f683541a7a25ee345133aa399'], + originalValue: 'b19a781f683541a7a25ee345133aa399', }, { category: 'host', - field: 'host.architecture', - values: ['armv7l'], - originalValue: ['armv7l'], + field: 'host.name', + values: ['raspberrypi'], + originalValue: 'raspberrypi', }, { - category: 'suricata', - field: 'suricata.eve.http.status', - values: ['206'], - originalValue: ['206'], + category: 'host', + field: 'host.os.codename', + values: ['stretch'], + originalValue: 'stretch', }, { - category: 'suricata', - field: 'suricata.eve.http.url', - values: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], - originalValue: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], + category: 'host', + field: 'host.os.family', + values: [''], + originalValue: '', }, { - category: 'url', - field: 'url.path', - values: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], - originalValue: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], + category: 'host', + field: 'host.os.kernel', + values: ['4.14.50-v7+'], + originalValue: '4.14.50-v7+', }, { - category: 'source', - field: 'source.port', - values: ['80'], - originalValue: ['80'], + category: 'host', + field: 'host.os.name', + values: ['Raspbian GNU/Linux'], + originalValue: 'Raspbian GNU/Linux', }, { - category: 'agent', - field: 'agent.id', - values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + category: 'host', + field: 'host.os.platform', + values: ['raspbian'], + originalValue: 'raspbian', }, { category: 'host', - field: 'host.containerized', - values: ['false'], - originalValue: ['false'], + field: 'host.os.version', + values: ['9 (stretch)'], + originalValue: '9 (stretch)', }, { - category: 'ecs', - field: 'ecs.version', - values: ['1.0.0-beta2'], - originalValue: ['1.0.0-beta2'], + category: 'http', + field: 'http.request.method', + values: ['get'], + originalValue: 'get', }, { - category: 'agent', - field: 'agent.version', - values: ['7.0.0'], - originalValue: ['7.0.0'], + category: 'http', + field: 'http.response.body.bytes', + values: [48277], + originalValue: 48277, }, { - category: 'suricata', - field: 'suricata.eve.fileinfo.stored', - values: ['false'], - originalValue: ['false'], + category: 'http', + field: 'http.response.status_code', + values: [206], + originalValue: 206, }, { - category: 'host', - field: 'host.os.family', - values: [''], - originalValue: [''], + category: 'input', + field: 'input.type', + values: ['log'], + originalValue: 'log', }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], + originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', }, { - category: 'suricata', - field: 'suricata.eve.src_ip', - values: ['54.239.219.210'], - originalValue: ['54.239.219.210'], + category: 'log', + field: 'log.file.path', + values: ['/var/log/suricata/eve.json'], + originalValue: '/var/log/suricata/eve.json', }, { - category: 'suricata', - field: 'suricata.eve.fileinfo.state', - values: ['CLOSED'], - originalValue: ['CLOSED'], + category: 'log', + field: 'log.offset', + values: [1856288115], + originalValue: 1856288115, }, { - category: 'destination', - field: 'destination.port', - values: ['40684'], - originalValue: ['40684'], + category: 'network', + field: 'network.name', + values: ['iot'], + originalValue: 'iot', }, { - category: 'traefik', - field: 'traefik.access.geoip.region_name', - values: ['Washington'], - originalValue: ['Washington'], + category: 'network', + field: 'network.protocol', + values: ['http'], + originalValue: 'http', }, { - category: 'source', - field: 'source.as.num', - values: ['16509'], - originalValue: ['16509'], + category: 'network', + field: 'network.transport', + values: ['tcp'], + originalValue: 'tcp', }, { - category: 'event', - field: 'event.end', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: ['2019-02-10T02:39:44.107Z'], + category: 'service', + field: 'service.type', + values: ['suricata'], + originalValue: 'suricata', }, { category: 'source', - field: 'source.geo.location', - values: ['{"long":-122.3341,"lat":47.6103}'], - originalValue: ['{"coordinates":[-122.3341,47.6103],"type":"Point"}'], + field: 'source.as.num', + values: [16509], + originalValue: 16509, }, { category: 'source', - field: 'source.domain', - values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - }, - { - category: 'suricata', - field: 'suricata.eve.fileinfo.size', - values: ['48277'], - originalValue: ['48277'], - }, - { - category: 'suricata', - field: 'suricata.eve.app_proto', - values: ['http'], - originalValue: ['http'], - }, - { - category: 'agent', - field: 'agent.type', - values: ['filebeat'], - originalValue: ['filebeat'], - }, - { - category: 'suricata', - field: 'suricata.eve.fileinfo.tx_id', - values: ['301'], - originalValue: ['301'], + field: 'source.as.org', + values: ['Amazon.com, Inc.'], + originalValue: 'Amazon.com, Inc.', }, { - category: 'event', - field: 'event.module', - values: ['suricata'], - originalValue: ['suricata'], + category: 'source', + field: 'source.domain', + values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], + originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', }, { - category: 'network', - field: 'network.protocol', - values: ['http'], - originalValue: ['http'], + category: 'source', + field: 'source.geo.city_name', + values: ['Seattle'], + originalValue: 'Seattle', }, { - category: 'host', - field: 'host.os.kernel', - values: ['4.14.50-v7+'], - originalValue: ['4.14.50-v7+'], + category: 'source', + field: 'source.geo.continent_name', + values: ['North America'], + originalValue: 'North America', }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], - originalValue: ['US'], + originalValue: 'US', }, { - category: '@version', - field: '@version', - values: ['1'], - originalValue: ['1'], - }, - { - category: 'host', - field: 'host.id', - values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: ['b19a781f683541a7a25ee345133aa399'], + category: 'source', + field: 'source.geo.location.lat', + values: [47.6103], + originalValue: 47.6103, }, { category: 'source', - field: 'source.as.org', - values: ['Amazon.com, Inc.'], - originalValue: ['Amazon.com, Inc.'], + field: 'source.geo.location.lon', + values: [-122.3341], + originalValue: -122.3341, }, { - category: 'suricata', - field: 'suricata.eve.timestamp', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: ['2019-02-10T02:39:44.107Z'], + category: 'source', + field: 'source.geo.region_iso_code', + values: ['US-WA'], + originalValue: 'US-WA', }, { - category: 'host', - field: 'host.os.codename', - values: ['stretch'], - originalValue: ['stretch'], + category: 'source', + field: 'source.geo.region_name', + values: ['Washington'], + originalValue: 'Washington', }, { category: 'source', - field: 'source.geo.continent_name', - values: ['North America'], - originalValue: ['North America'], + field: 'source.ip', + values: ['54.239.219.210'], + originalValue: '54.239.219.210', }, { - category: 'network', - field: 'network.name', - values: ['iot'], - originalValue: ['iot'], + category: 'source', + field: 'source.port', + values: [80], + originalValue: 80, }, { category: 'suricata', - field: 'suricata.eve.http.http_method', - values: ['get'], - originalValue: ['get'], - }, - { - category: 'traefik', - field: 'traefik.access.geoip.continent_name', - values: ['North America'], - originalValue: ['North America'], + field: 'suricata.eve.fileinfo.state', + values: ['CLOSED'], + originalValue: 'CLOSED', }, { - category: 'file', - field: 'file.size', - values: ['48277'], - originalValue: ['48277'], + category: 'suricata', + field: 'suricata.eve.fileinfo.tx_id', + values: [301], + originalValue: 301, }, { - category: 'destination', - field: 'destination.ip', - values: ['10.100.7.196'], - originalValue: ['10.100.7.196'], + category: 'suricata', + field: 'suricata.eve.flow_id', + values: [196625917175466], + originalValue: 196625917175466, }, { category: 'suricata', - field: 'suricata.eve.http.length', - values: ['48277'], - originalValue: ['48277'], + field: 'suricata.eve.http.http_content_type', + values: ['video/mp4'], + originalValue: 'video/mp4', }, { - category: 'http', - field: 'http.response.body.bytes', - values: ['48277'], - originalValue: ['48277'], + category: 'suricata', + field: 'suricata.eve.http.protocol', + values: ['HTTP/1.1'], + originalValue: 'HTTP/1.1', }, { category: 'suricata', - field: 'suricata.eve.fileinfo.filename', - values: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], - originalValue: [ - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], + field: 'suricata.eve.in_iface', + values: ['eth0'], + originalValue: 'eth0', }, { - category: 'suricata', - field: 'suricata.eve.dest_ip', - values: ['10.100.7.196'], - originalValue: ['10.100.7.196'], + category: 'base', + field: 'tags', + values: ['suricata'], + originalValue: ['suricata'], }, { - category: 'network', - field: 'network.transport', - values: ['tcp'], - originalValue: ['tcp'], + category: 'url', + field: 'url.domain', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', }, { category: 'url', @@ -481,81 +390,35 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: [ + originalValue: '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', - ], - }, - { - category: 'base', - field: '@timestamp', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: ['2019-02-10T02:39:44.107Z'], - }, - { - category: 'host', - field: 'host.os.platform', - values: ['raspbian'], - originalValue: ['raspbian'], - }, - { - category: 'suricata', - field: 'suricata.eve.dest_port', - values: ['40684'], - originalValue: ['40684'], - }, - { - category: 'event', - field: 'event.type', - values: ['fileinfo'], - originalValue: ['fileinfo'], - }, - { - category: 'log', - field: 'log.file.path', - values: ['/var/log/suricata/eve.json'], - originalValue: ['/var/log/suricata/eve.json'], }, { category: 'url', - field: 'url.domain', - values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - }, - { - category: 'agent', - field: 'agent.ephemeral_id', - values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - }, - { - category: 'suricata', - field: 'suricata.eve.http.http_content_type', - values: ['video/mp4'], - originalValue: ['video/mp4'], - }, - { - category: 'event', - field: 'event.dataset', - values: ['suricata.eve'], - originalValue: ['suricata.eve'], + field: 'url.path', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: ['filebeat-7.0.0-iot-2019.06'], + originalValue: 'filebeat-7.0.0-iot-2019.06', }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: ['QRhG1WgBqd-n62SwZYDT'], + originalValue: 'QRhG1WgBqd-n62SwZYDT', }, { category: '_score', field: '_score', - values: ['1'], - originalValue: ['1'], + values: [1], + originalValue: 1, }, ]; @@ -589,12 +452,8 @@ export default function ({ getService }: FtrProviderContext) { eventId: ID, }) .expect(200); - expect( - sortBy(detailsData, 'name').map((item) => { - const { __typename, ...rest } = item; - return rest; - }) - ).to.eql(sortBy(EXPECTED_DATA, 'name')); + + expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); }); it('Make sure that we get kpi data', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index b1d6a13b773002..1ae6aa80b219f4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -34,6 +34,8 @@ import { createExceptionListItem, waitForSignalsToBePresent, getSignalsByIds, + findImmutableRuleById, + getPrePackagedRulesStatus, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -394,6 +396,83 @@ export default ({ getService }: FtrProviderContext) => { ]); }); + it('should not change the immutable tags when adding a second exception list to an immutable rule through patch', async () => { + await installPrePackagedRules(supertest); + + const { id, list_id, namespace_type, type } = await createExceptionList( + supertest, + getCreateExceptionListMinimalSchemaMock() + ); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + const body = await findImmutableRuleById(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + expect(body.data.length).to.eql(1); // should have only one length to the data set, otherwise we have duplicates or the tags were removed and that is incredibly bad. + + const bodyToCompare = removeServerGeneratedProperties(body.data[0]); + expect(bodyToCompare.rule_id).to.eql(immutableRule.rule_id); // Rule id should not change with a a patch + expect(bodyToCompare.immutable).to.eql(immutableRule.immutable); // Immutable should always stay the same which is true and never flip to false. + expect(bodyToCompare.version).to.eql(immutableRule.version); // The version should never update on a patch + }); + + it('should not change count of prepacked rules when adding a second exception list to an immutable rule through patch. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => { + await installPrePackagedRules(supertest); + + const { id, list_id, namespace_type, type } = await createExceptionList( + supertest, + getCreateExceptionListMinimalSchemaMock() + ); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + const status = await getPrePackagedRulesStatus(supertest); + expect(status.rules_not_installed).to.eql(0); + }); + describe('tests with auditbeat data', () => { beforeEach(async () => { await createSignalsIndex(supertest); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 7f299fc5801388..b6d88b657f25c1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -14,6 +14,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { this.tags('ciGroup11'); loadTestFile(require.resolve('./add_actions')); + loadTestFile(require.resolve('./update_actions')); loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts new file mode 100644 index 00000000000000..257c6a4286982d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + removeServerGeneratedProperties, + getRuleWithWebHookAction, + getSimpleRuleOutputWithWebHookAction, + waitForRuleSuccessOrStatus, + createRule, + getSimpleRule, + updateRule, + installPrePackagedRules, + getRule, + createNewAction, + findImmutableRuleById, + getPrePackagedRulesStatus, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('update_actions', () => { + describe('updating actions', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to create a new webhook action and update a rule with the webhook action', async () => { + const hookAction = await createNewAction(supertest); + const rule = getSimpleRule(); + await createRule(supertest, rule); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, rule); + const updatedRule = await updateRule(supertest, ruleToUpdate); + const bodyToCompare = removeServerGeneratedProperties(updatedRule); + + const expected = { + ...getSimpleRuleOutputWithWebHookAction(`${bodyToCompare.actions?.[0].id}`), + version: 2, // version bump is required since this is an updated rule and this is part of the testing that we do bump the version number on update + }; + expect(bodyToCompare).to.eql(expected); + }); + + it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => { + const hookAction = await createNewAction(supertest); + const rule = getSimpleRule(); + await createRule(supertest, rule); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, rule); + const updatedRule = await updateRule(supertest, ruleToUpdate); + await waitForRuleSuccessOrStatus(supertest, updatedRule.id); + + // expected result for status should be 'succeeded' + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [updatedRule.id] }) + .expect(200); + expect(body[updatedRule.id].current_status.status).to.eql('succeeded'); + }); + + it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { + const hookAction = await createNewAction(supertest); + const rule = getSimpleRule(); + await createRule(supertest, rule); + const ruleToUpdate: CreateRulesSchema = { + ...getRuleWithWebHookAction(hookAction.id, true, rule), + meta: {}, // create a rule with the action attached and a meta field + }; + const updatedRule = await updateRule(supertest, ruleToUpdate); + await waitForRuleSuccessOrStatus(supertest, updatedRule.id); + + // expected result for status should be 'succeeded' + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [updatedRule.id] }) + .expect(200); + expect(body[updatedRule.id].current_status.status).to.eql('succeeded'); + }); + + it('should be able to create a new webhook action and attach it to an immutable rule', async () => { + await installPrePackagedRules(supertest); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); + const updatedRule = await updateRule(supertest, ruleToUpdate); + const bodyToCompare = removeServerGeneratedProperties(updatedRule); + + const expected = { + ...getSimpleRuleOutputWithWebHookAction(`${bodyToCompare.actions?.[0].id}`), + rule_id: immutableRule.rule_id, // Rule id should match the same as the immutable rule + version: immutableRule.version, // This version number should not change when an immutable rule is updated + immutable: true, // It should stay immutable true when returning + }; + expect(bodyToCompare).to.eql(expected); + }); + + it('should be able to create a new webhook action, attach it to an immutable rule and the count of prepackaged rules should not increase. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => { + await installPrePackagedRules(supertest); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); + await updateRule(supertest, ruleToUpdate); + + const status = await getPrePackagedRulesStatus(supertest); + expect(status.rules_not_installed).to.eql(0); + }); + + it('should be able to create a new webhook action, attach it to an immutable rule and the rule should stay immutable when searching against immutable tags', async () => { + await installPrePackagedRules(supertest); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + const immutableRule = await getRule(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); + await updateRule(supertest, ruleToUpdate); + const body = await findImmutableRuleById(supertest, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + expect(body.data.length).to.eql(1); // should have only one length to the data set, otherwise we have duplicates or the tags were removed and that is incredibly bad. + const bodyToCompare = removeServerGeneratedProperties(body.data[0]); + const expected = { + ...getSimpleRuleOutputWithWebHookAction(`${bodyToCompare.actions?.[0].id}`), + rule_id: immutableRule.rule_id, // Rule id should match the same as the immutable rule + version: immutableRule.version, // This version number should not change when an immutable rule is updated + immutable: true, // It should stay immutable true when returning + }; + expect(bodyToCompare).to.eql(expected); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 71390400c359be..158247ee244ddc 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -11,6 +11,7 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; +import { PrePackagedRulesAndTimelinesStatusSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response'; import { NonEmptyEntriesArray } from '../../plugins/lists/common/schemas'; import { getCreateExceptionListDetectionSchemaMock } from '../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { @@ -38,6 +39,7 @@ import { DETECTION_ENGINE_PREPACKAGED_URL, DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_RULES_URL, + INTERNAL_IMMUTABLE_KEY, INTERNAL_RULE_ID_KEY, } from '../../plugins/security_solution/common/constants'; import { getCreateExceptionListItemMinimalSchemaMockWithoutId } from '../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; @@ -674,20 +676,27 @@ export const getWebHookAction = () => ({ name: 'Some connector', }); -export const getRuleWithWebHookAction = (id: string, enabled = false): CreateRulesSchema => ({ - ...getSimpleRule('rule-1', enabled), - throttle: 'rule', - actions: [ - { - group: 'default', - id, - params: { - body: '{}', +export const getRuleWithWebHookAction = ( + id: string, + enabled = false, + rule?: QueryCreateSchema +): CreateRulesSchema | UpdateRulesSchema => { + const finalRule = rule != null ? { ...rule, enabled } : getSimpleRule('rule-1', enabled); + return { + ...finalRule, + throttle: 'rule', + actions: [ + { + group: 'default', + id, + params: { + body: '{}', + }, + action_type_id: '.webhook', }, - action_type_id: '.webhook', - }, - ], -}); + ], + }; +}; export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial => ({ ...getSimpleRuleOutput(), @@ -830,6 +839,78 @@ export const createRule = async ( return body; }; +/** + * Helper to cut down on the noise in some of the tests. This checks for + * an expected 200 still and does not do any retries. + * @param supertest The supertest deps + * @param rule The rule to create + */ +export const updateRule = async ( + supertest: SuperTest, + updatedRule: UpdateRulesSchema +): Promise => { + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + return body; +}; + +/** + * Helper to cut down on the noise in some of the tests. This + * creates a new action and expects a 200 and does not do any retries. + * @param supertest The supertest deps + */ +export const createNewAction = async (supertest: SuperTest) => { + const { body } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + return body; +}; + +/** + * Helper to cut down on the noise in some of the tests. This + * creates a new action and expects a 200 and does not do any retries. + * @param supertest The supertest deps + */ +export const findImmutableRuleById = async ( + supertest: SuperTest, + ruleId: string +): Promise<{ + page: number; + perPage: number; + total: number; + data: FullResponseSchema[]; +}> => { + const { body } = await supertest + .get( + `${DETECTION_ENGINE_RULES_URL}/_find?filter=alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + return body; +}; + +/** + * Helper to cut down on the noise in some of the tests. This + * creates a new action and expects a 200 and does not do any retries. + * @param supertest The supertest deps + */ +export const getPrePackagedRulesStatus = async ( + supertest: SuperTest +): Promise => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + return body; +}; + /** * Helper to cut down on the noise in some of the tests. This checks for * an expected 200 still and does not try to any retries. Creates exception lists diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 0d0749aa8e913a..9f016ab044a90e 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -26,8 +26,10 @@ export default function ({ getService }: FtrProviderContext) { }); describe('POST /api/fleet/agent_policies', () => { - it('should work with valid values', async () => { - await supertest + it('should work with valid minimum required values', async () => { + const { + body: { item: createdPolicy }, + } = await supertest .post(`/api/fleet/agent_policies`) .set('kbn-xsrf', 'xxxx') .send({ @@ -35,6 +37,28 @@ export default function ({ getService }: FtrProviderContext) { namespace: 'default', }) .expect(200); + + const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + const json = getRes.body; + expect(json.item.is_managed).to.equal(false); + }); + + it('sets given is_managed value', async () => { + const { + body: { item: createdPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST2', + namespace: 'default', + is_managed: true, + }) + .expect(200); + + const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + const json = getRes.body; + expect(json.item.is_managed).to.equal(true); }); it('should return a 400 with an empty namespace', async () => { @@ -108,6 +132,7 @@ export default function ({ getService }: FtrProviderContext) { expect(newPolicy).to.eql({ name: 'Copied policy', description: 'Test', + is_managed: false, namespace: 'default', monitoring_enabled: ['logs', 'metrics'], revision: 1, @@ -161,6 +186,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('PUT /api/fleet/agent_policies/{agentPolicyId}', () => { + let agentPolicyId: undefined | string; it('should work with valid values', async () => { const { body: { item: originalPolicy }, @@ -173,11 +199,11 @@ export default function ({ getService }: FtrProviderContext) { namespace: 'default', }) .expect(200); - + agentPolicyId = originalPolicy.id; const { body: { item: updatedPolicy }, } = await supertest - .put(`/api/fleet/agent_policies/${originalPolicy.id}`) + .put(`/api/fleet/agent_policies/${agentPolicyId}`) .set('kbn-xsrf', 'xxxx') .send({ name: 'Updated name', @@ -193,12 +219,31 @@ export default function ({ getService }: FtrProviderContext) { name: 'Updated name', description: 'Updated description', namespace: 'default', + is_managed: false, revision: 2, updated_by: 'elastic', package_policies: [], }); }); + it('sets given is_managed value', async () => { + const { + body: { item: createdPolicy }, + } = await supertest + .put(`/api/fleet/agent_policies/${agentPolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST2', + namespace: 'default', + is_managed: true, + }) + .expect(200); + + const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + const json = getRes.body; + expect(json.item.is_managed).to.equal(true); + }); + it('should return a 409 if policy already exists with name given', async () => { const sharedBody = { name: 'Initial name', diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index e17e779e4217b0..a31fa862f74205 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -16,10 +16,10 @@ export default function (providerContext: FtrProviderContext) { describe('fleet_reassign_agent', () => { setupFleetAndAgents(providerContext); - before(async () => { + beforeEach(async () => { await esArchiver.loadIfNeeded('fleet/agents'); }); - after(async () => { + afterEach(async () => { await esArchiver.unload('fleet/agents'); }); @@ -31,7 +31,7 @@ export default function (providerContext: FtrProviderContext) { policy_id: 'policy2', }) .expect(200); - const { body } = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); + const { body } = await supertest.get(`/api/fleet/agents/agent1`); expect(body.item.policy_id).to.eql('policy2'); }); @@ -88,5 +88,34 @@ export default function (providerContext: FtrProviderContext) { }) .expect(404); }); + + it('can reassign from unmanaged policy to unmanaged', async () => { + // policy2 is not managed + // reassign succeeds + await supertest + .put(`/api/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(200); + }); + it('cannot reassign from unmanaged policy to managed', async () => { + // agent1 is enrolled in policy1. set policy1 to managed + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: true }) + .expect(200); + + // reassign fails + await supertest + .put(`/api/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(400); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index 3cafc86602d3b2..85bcce824dd51b 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -65,17 +65,28 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('should allow to unenroll single agent', async () => { + it('/agents/{agent_id}/unenroll should fail for managed policy', async () => { + // set policy to managed await supertest - .post(`/api/fleet/agents/agent1/unenroll`) + .put(`/api/fleet/agent_policies/policy1`) .set('kbn-xsrf', 'xxx') - .send({ - force: true, - }) + .send({ name: 'Test policy', namespace: 'default', is_managed: true }) + .expect(200); + + await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').expect(400); + }); + + it('/agents/{agent_id}/unenroll should allow from unmanaged policy', async () => { + // set policy to unmanaged + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: false }) .expect(200); + await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').expect(200); }); - it('should invalidate related API keys', async () => { + it('/agents/{agent_id}/unenroll { force: true } should invalidate related API keys', async () => { await supertest .post(`/api/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') @@ -97,7 +108,44 @@ export default function (providerContext: FtrProviderContext) { expect(outputAPIKeys[0].invalidated).eql(true); }); - it('should allow to unenroll multiple agents by id', async () => { + it('/agents/{agent_id}/bulk_unenroll should not allow unenroll from managed policy', async () => { + // set policy to managed + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: true }) + .expect(200); + + // try to unenroll + await supertest + .post(`/api/fleet/agents/bulk_unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + }) + // http request succeeds + .expect(200); + + // but agents are still enrolled + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent2`), + supertest.get(`/api/fleet/agents/agent3`), + ]); + expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('undefined'); + expect(typeof agent2data.body.item.unenrolled_at).to.eql('undefined'); + expect(agent2data.body.item.active).to.eql(true); + expect(typeof agent3data.body.item.unenrollment_started_at).to.be('undefined'); + expect(typeof agent3data.body.item.unenrolled_at).to.be('undefined'); + expect(agent2data.body.item.active).to.eql(true); + }); + + it('/agents/{agent_id}/bulk_unenroll should allow to unenroll multiple agents by id from an unmanaged policy', async () => { + // set policy to unmanaged + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: false }) + .expect(200); await supertest .post(`/api/fleet/agents/bulk_unenroll`) .set('kbn-xsrf', 'xxx') @@ -106,8 +154,8 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); const [agent2data, agent3data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`), + supertest.get(`/api/fleet/agents/agent3`), ]); expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('string'); expect(agent2data.body.item.active).to.eql(true); @@ -115,7 +163,7 @@ export default function (providerContext: FtrProviderContext) { expect(agent2data.body.item.active).to.eql(true); }); - it('should allow to unenroll multiple agents by kuery', async () => { + it('/agents/{agent_id}/bulk_unenroll should allow to unenroll multiple agents by kuery', async () => { await supertest .post(`/api/fleet/agents/bulk_unenroll`) .set('kbn-xsrf', 'xxx') @@ -125,7 +173,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + const { body } = await supertest.get(`/api/fleet/agents`); expect(body.total).to.eql(0); }); });